Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@

## Security & Configuration Tips
- Do not commit populated `settings.toml`. Copy from `settings.tpl.toml` to `~/.mostro/settings.toml` for local runs.
- Protect LND credentials before `make docker-build`.
- Protect LND credentials before `make docker-build`. The admin macaroon is spend-capable: copy it with `install -m 600` (never plain `cp`, which inherits the source or destination mode) and keep its directory at `0700`.
- Scrub logs that might leak invoices or Nostr keys; rotate secrets promptly if exposed.
19 changes: 19 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ Here some parameters you might want to change:
- **nsec_privkey** : Your mostro private key
- **relays** : List of relays you want to connect to

### Protect the admin macaroon

The admin macaroon is a spend-capable credential: any user who can read it has full control of the LND node, including the funds escrowed in Mostro's hold invoices. The `mostro` service account created above must be able to read it, and no one else.

If LND runs on this same VPS, grant access through the node's group instead of loosening the file (LND creates `admin.macaroon` with mode `0640`):
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Outdated

```bash
usermod -aG lnd mostro
```

If you copy the macaroon into `/opt/mostro` instead, install it owner-readable only and hand it to the service account — do not use plain `cp`, which keeps whatever mode the source file or an existing destination happens to have:

```bash
install -d -m 700 -o mostro -g mostro /opt/mostro/lnd
install -m 600 -o mostro -g mostro /path/to/lnd/admin.macaroon /opt/mostro/lnd/admin.macaroon
```

Then point `lnd_macaroon_file` at `/opt/mostro/lnd/admin.macaroon`.

## Database

The data is saved in a sqlite db file named by default `mostro.db`, this file is saved on the root directory of the project and can be change just editing the `url` var on the `[database]` section in `settings.toml` file.
Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ VERSION := $(shell grep "^version = " Cargo.toml | sed "s/version = \"\(.*\)\"/\
docker-build:
@set -o pipefail; \
cd docker && \
mkdir -p config/lnd && \
install -d -m 700 config/lnd && \
echo "Checking LND files..." && \
echo "LND_CERT_FILE=$${LND_CERT_FILE}" && \
echo "LND_MACAROON_FILE=$${LND_MACAROON_FILE}" && \
Expand All @@ -27,8 +27,11 @@ docker-build:
exit 1; \
fi && \
echo "Copying LND cert and macaroon to docker config" && \
cp -v $${LND_CERT_FILE} config/lnd/tls.cert && \
cp -v $${LND_MACAROON_FILE} config/lnd/admin.macaroon && \
install -m 644 "$${LND_CERT_FILE}" config/lnd/tls.cert && \
install -m 600 "$${LND_MACAROON_FILE}" config/lnd/admin.macaroon && \
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
echo "Wrote config/lnd/tls.cert (mode 644) and config/lnd/admin.macaroon (mode 600)" && \
echo "Note: the container runs as uid/gid 1000. If they are owned by another user," && \
echo " run: sudo chown -R 1000:1000 docker/config/lnd" && \
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Outdated
echo "Building docker image" && \
docker compose build

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ This starts:
- Mostro daemon (exposed via configured relays)
- Local Nostr relay (port 7000 by default)

`make docker-build` installs the LND admin macaroon into `docker/config/lnd/` with mode `0600`, since it grants full control of your node. The container runs as uid/gid 1000, so run `sudo chown -R 1000:1000 docker/config/lnd` if you built as a different user.

**Stop**: `make docker-down`

For detailed Docker setup, see [docker/README.md](docker/README.md).
Expand Down Expand Up @@ -565,6 +567,8 @@ payment_retries_interval = 60 # seconds between retries

**Required**: LND connection details. Mostro needs admin macaroon for hold invoice management.

**Permissions**: the admin macaroon is a spend-capable credential — anyone who can read it controls the node, including the funds escrowed in Mostro's hold invoices. Keep it readable only by the user running `mostrod` (`chmod 600 /path/to/lnd/admin.macaroon`). mostrod logs a warning at startup when the file is reachable by other users on the host.

---

#### Nostr Configuration
Expand Down
8 changes: 7 additions & 1 deletion docker/ENV_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ This document describes the environment variables used by the Docker setup.
- `LND_MACAROON_FILE`: Path to the LND admin macaroon file on your host system
- Example: `~/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon`

These files are copied to `docker/config/lnd/` during the build process.
These files are copied to `docker/config/lnd/` during the build process: the cert with mode `0644`, the admin macaroon with mode `0600`, both inside a directory with mode `0700`. The macaroon grants full control of your LND node, so it is never left readable by other users on the host.

The container runs as uid/gid 1000, so that directory and its contents must be owned by uid 1000 for mostrod to reach them. If you run `make docker-build` as another user, fix the ownership afterwards:

```sh
sudo chown -R 1000:1000 docker/config/lnd
```

## Optional Variables

Expand Down
30 changes: 22 additions & 8 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ To build and run the Docker container using Docker Compose, follow these steps:
make docker-build
```

The admin macaroon grants full control of your LND node, so `make docker-build` writes it to `config/lnd/admin.macaroon` with mode `0600` (owner only) inside a `config/lnd` directory with mode `0700`. The container runs as uid/gid 1000, so that directory and its contents must be owned by uid 1000 for mostrod to reach the macaroon. If you ran `make docker-build` as a different user, fix the ownership:

```sh
sudo chown -R 1000:1000 docker/config/lnd
```

4. [Optional] Set the `MOSTRO_RELAY_LOCAL_PORT` environment variable to the port you want to use for the local relay (defaults to 7000 if not set). This can be set before running `make docker-up`:

```sh
Expand Down Expand Up @@ -89,19 +95,22 @@ You can run the plain Mostro image without building locally. Use a single **conf
**Option A — download the template** (from the [settings.tpl.toml](https://github.com/MostroP2P/mostro/blob/main/settings.tpl.toml) repo file):

```sh
mkdir -p ~/mostro-config/lnd
mkdir -p ~/mostro-config
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Outdated
install -d -m 700 ~/mostro-config/lnd
curl -sL https://raw.githubusercontent.com/MostroP2P/mostro/main/settings.tpl.toml -o ~/mostro-config/settings.toml
```

**Option B — use the entrypoint default:** run the container once with an empty config dir; the entrypoint copies a default `settings.toml` (from the image, built from `settings.tpl.toml`) into `/config`. Stop the container, edit the file on the host (e.g. `~/mostro-config/settings.toml`), then start the container again.

2. Copy your LND TLS cert and macaroon into the config dir (so they appear at `/config/lnd/` in the container):
2. Copy your LND TLS cert and macaroon into the config dir (so they appear at `/config/lnd/` in the container). Use `install` rather than `cp`: `cp` keeps whatever mode the source file (or an already existing destination file) happens to have, while `install -m` sets the mode explicitly. The admin macaroon grants full control of your LND node, so it must not be readable by other users on the host:

```sh
cp /path/to/your/tls.cert ~/mostro-config/lnd/tls.cert
cp /path/to/your/admin.macaroon ~/mostro-config/lnd/admin.macaroon
install -m 644 /path/to/your/tls.cert ~/mostro-config/lnd/tls.cert
install -m 600 /path/to/your/admin.macaroon ~/mostro-config/lnd/admin.macaroon
```

The container runs as uid/gid 1000, and mode `0600` on the macaroon inside a `0700` directory means only their owner can reach the file. If your user is not uid 1000, either hand the directory to the container's user with `sudo chown -R 1000:1000 ~/mostro-config/lnd`, or keep your own ownership and run the container as yourself by adding `--user $(id -u):$(id -g)` to the `docker run` command in step 4.

3. Edit `~/mostro-config/settings.toml`: set `nsec_privkey`, `relays`, and for Docker set `lnd_cert_file` / `lnd_macaroon_file` to `/config/lnd/...`, `lnd_grpc_host` (e.g. `https://host.docker.internal:10009`), and `[database]` `url = "sqlite:///config/mostro.db"`.

4. Run the container. On Linux, add `--add-host=host.docker.internal:host-gateway` so the container can reach LND on the host:
Expand All @@ -126,9 +135,12 @@ Steps to run the plain Mostro image on a VPS (no repo clone; image from Docker H
2. **Create a config directory** (e.g. `/opt/mostro` or `~/mostro-config`):

```sh
mkdir -p /opt/mostro/lnd
mkdir -p /opt/mostro
install -d -m 700 -o 1000 -g 1000 /opt/mostro/lnd
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
```

These steps run as root, while the container runs as uid/gid 1000, so the directory holding the LND credentials is created owner-only and handed to the container's user (see step 4).

3. **Get the settings template** into that directory as `settings.toml`:

- Either run the container once with an empty config dir; the entrypoint will copy the default template to `/config/settings.toml`. Stop the container, then edit the file on the host.
Expand All @@ -138,13 +150,15 @@ Steps to run the plain Mostro image on a VPS (no repo clone; image from Docker H
curl -sL https://raw.githubusercontent.com/MostroP2P/mostro/main/settings.tpl.toml -o /opt/mostro/settings.toml
```

4. **Put LND files** in the config dir so they appear at `/config/lnd/` in the container:
4. **Put LND files** in the config dir so they appear at `/config/lnd/` in the container. Use `install -m` rather than `cp`, which would keep whatever mode the source file (or an already existing destination file) happens to have:

```sh
cp /path/to/lnd/tls.cert /opt/mostro/lnd/tls.cert
cp /path/to/lnd/admin.macaroon /opt/mostro/lnd/admin.macaroon
install -m 644 /path/to/lnd/tls.cert /opt/mostro/lnd/tls.cert
install -m 600 -o 1000 -g 1000 /path/to/lnd/admin.macaroon /opt/mostro/lnd/admin.macaroon
```

The admin macaroon grants full control of your LND node — anyone who reads it can move the funds escrowed in Mostro's hold invoices — so it is installed owner-readable only, and `-o 1000 -g 1000` hands it to the container's user (as the `0700` directory from step 2 already was). Without that ownership, mode `0600` would leave mostrod unable to read the macaroon. (`-o`/`-g` require root; as a non-root user, drop them and run the steps as the account that owns the config dir.)

(If LND is on another host, you only need the cert and macaroon copied here; point `lnd_grpc_host` at that host in step 5.)

5. **Edit `/opt/mostro/settings.toml`**:
Expand Down
8 changes: 8 additions & 0 deletions docs/STARTUP_AND_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ Configuration is loaded from `~/.mostro/settings.toml` (template: `settings.tpl.
**Lightning** (`src/config/types.rs:27-46`):
- `lnd_cert_file` (String): Path to LND TLS certificate
- `lnd_macaroon_file` (String): Path to LND macaroon auth file
- The admin macaroon is spend-capable: any account that can read it controls
the node, including the funds escrowed in Mostro's hold invoices. Keep it
readable only by the user running `mostrod` (`chmod 600`).
- At startup (Lightning mode only, right before the LND connection is opened)
`config::permissions::warn_if_other_accessible` logs a warning when the file
is reachable by users outside its owner and group. The check is advisory —
the daemon still starts — and tolerates `0640`, the mode LND itself writes
the macaroon with, so reaching it through the node's group stays supported.
- `lnd_grpc_host` (String): LND gRPC endpoint URL
- `invoice_expiration_window` (u32): Required invoice validity window in seconds (default: 3600)
- `hold_invoice_cltv_delta` (u32): Hold invoice CLTV delta in blocks (default: 144)
Expand Down
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Mostro module for configurataion settings
pub mod constants;
pub mod permissions;
pub mod secret;
pub mod settings;
/// This module provides functionality to manage and initialize settings for the Mostro application.
Expand Down
142 changes: 142 additions & 0 deletions src/config/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! Startup checks on the filesystem permissions of the credential files
//! mostrod reads.
//!
//! Kept in its own module — rather than folded into `config::util` — because
//! these checks are about the files named by the settings, not about loading
//! the settings themselves.

use std::path::Path;

/// Warn when a credential file is reachable by users outside its owner and
/// group.
///
/// The check is advisory: a node whose macaroon is `0644` still starts, it
/// just says so out loud once per boot. Refusing to start would turn a
/// hardening gap into an outage on the next upgrade for every operator who
/// already runs that way.
///
/// Empty paths and files that cannot be stat'ed are ignored: an unset or
/// unreadable path is not a permissions problem, and the real failure is
/// reported with far more context by whoever opens the file (for the macaroon,
/// `LndConnector::new`).
pub fn warn_if_other_accessible(path: &str, label: &str) {
if path.is_empty() {
return;
}

if let Some(mode) = other_accessible_mode(Path::new(path)) {
tracing::warn!(
"{label} ({path}) has permissions {mode:04o}: it is reachable by users other than \
its owner and group. Restrict it to the account running mostrod with: chmod 600 {path}"
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
Outdated
);
}
}

/// The file's permission bits when the "other" class has any of them set,
/// `None` otherwise.
///
/// Group access is deliberately tolerated: LND itself creates
/// `admin.macaroon` with mode `0640`, and granting a service account access
/// through the node's group is a legitimate deployment, not a finding.
///
/// `metadata` follows symlinks on purpose — pointing `lnd_macaroon_file` at a
/// link is common, and what matters is the mode of the file that is actually
/// read.
#[cfg(unix)]
fn other_accessible_mode(path: &Path) -> Option<u32> {
use std::os::unix::fs::PermissionsExt;

let mode = std::fs::metadata(path).ok()?.permissions().mode() & 0o777;
(mode & 0o007 != 0).then_some(mode)
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
}

/// Non-Unix platforms have no POSIX permission bits to inspect.
#[cfg(not(unix))]
fn other_accessible_mode(_path: &Path) -> Option<u32> {
None
}

#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

fn temp_dir(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("mostro-permissions-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}

fn macaroon_with_mode(tag: &str, mode: u32) -> PathBuf {
let path = temp_dir(tag).join("admin.macaroon");
std::fs::write(&path, b"macaroon").expect("write macaroon");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
.expect("set permissions");
path
}

#[test]
fn owner_only_is_accepted() {
let path = macaroon_with_mode("owner-only", 0o600);
assert_eq!(other_accessible_mode(&path), None);
}

#[test]
fn group_readable_is_accepted() {
// 0640 is the mode LND writes admin.macaroon with, and reaching it
// through the node's group is a supported setup.
let path = macaroon_with_mode("group-read", 0o640);
assert_eq!(other_accessible_mode(&path), None);
}

#[test]
fn world_readable_is_reported() {
let path = macaroon_with_mode("world-read", 0o644);
assert_eq!(other_accessible_mode(&path), Some(0o644));
}

#[test]
fn other_read_without_group_read_is_reported() {
let path = macaroon_with_mode("other-read", 0o604);
assert_eq!(other_accessible_mode(&path), Some(0o604));
}

#[test]
fn world_writable_is_reported() {
// Not a disclosure by itself, but a local account that can replace the
// credential mostrod authenticates with is the same class of problem.
let path = macaroon_with_mode("world-write", 0o602);
assert_eq!(other_accessible_mode(&path), Some(0o602));
}

#[test]
fn symlink_reports_the_target_mode() {
let dir = temp_dir("symlink");
let target = dir.join("real.macaroon");
std::fs::write(&target, b"macaroon").expect("write macaroon");
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o644))
.expect("set permissions");

let link = dir.join("linked.macaroon");
let _ = std::fs::remove_file(&link);
std::os::unix::fs::symlink(&target, &link).expect("create symlink");

assert_eq!(other_accessible_mode(&link), Some(0o644));
}

#[test]
fn missing_file_is_ignored() {
assert_eq!(
other_accessible_mode(Path::new("/definitely/not/here.macaroon")),
None
);
}

#[test]
fn empty_and_missing_paths_do_not_panic() {
warn_if_other_accessible("", "LND admin macaroon");
warn_if_other_accessible("/definitely/not/here.macaroon", "LND admin macaroon");
}
}
13 changes: 12 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use crate::app::context::AppContext;
use crate::app::{run, run_cashu};
use crate::cli::settings_init;
use crate::config::{
get_db_pool, Settings, DB_POOL, LN_STATUS, MESSAGE_QUEUES, MOSTRO_CONFIG, NOSTR_CLIENT,
get_db_pool, permissions, Settings, DB_POOL, LN_STATUS, MESSAGE_QUEUES, MOSTRO_CONFIG,
NOSTR_CLIENT,
};
use crate::db::find_held_invoices;
use crate::lightning::LnStatus;
Expand Down Expand Up @@ -221,6 +222,16 @@ async fn main() -> Result<()> {
return run_cashu(ctx).await;
}

// The admin macaroon is spend-capable: whoever reads it controls the node,
// including the funds escrowed in the hold invoices mostrod manages. Say so
// once per boot when the file is left reachable by other local accounts —
// the documented deployment flows copy it around, and a bad mode is
// otherwise invisible until it is abused.
permissions::warn_if_other_accessible(
&Settings::get_ln().lnd_macaroon_file,
"LND admin macaroon",
);

let mut ln_client = LndConnector::new().await?;
let ln_status = ln_client.get_node_info().await?;
let ln_status = LnStatus::from_get_info_response(ln_status);
Expand Down