Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The Docker build context is the repository root (docker/compose.yml sets
# `context: ..`) and both Dockerfiles pull it into the builder stage with
# `COPY . .`. `make docker-build` stages the LND cert and admin macaroon in
# docker/config/lnd/ right before `docker compose build`, so without this entry
# a spend-capable credential would be shipped into the build context and left
# in the builder layer and its cache. .gitignore already keeps it out of git,
# but Docker does not read .gitignore.
docker/config/
docker/.env
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. Access should reach no further than the `mostro` service account created above and the LND account the node already runs as.

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`):

```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 by default. If your user is not uid 1000," && \
echo ' export MOSTRO_CONTAINER_USER=$$(id -u):$$(id -g) before make docker-up' && \
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 by default; if your user is not uid 1000, `export MOSTRO_CONTAINER_USER=$(id -u):$(id -g)` so it runs as you and can read the macaroon.

**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 account running `mostrod`, or by that account and LND's group (`chmod 600` for a private copy, `chmod o=` to keep group access). mostrod logs a warning at startup when the file's `other` permission bits are set.

---

#### 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,14 +10,20 @@ 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 copies belong to the user that ran the command, and the container runs as uid/gid 1000 by default. If your user is not uid 1000, run the container as yourself with the optional variable below rather than handing the config directory over to uid 1000.

## Optional Variables

- `MOSTRO_RELAY_LOCAL_PORT`: Port number for the local Nostr relay (defaults to 7000)
- Used in `compose.yml` for port mapping
- Example: `export MOSTRO_RELAY_LOCAL_PORT=7000`

- `MOSTRO_CONTAINER_USER`: uid/gid the `mostro` container runs as (defaults to `1000:1000`, the image's `mostrouser`)
- Set it when your host user is not uid 1000, so the container can read the `0600` macaroon and write `mostro.db` in the mounted config directory
- Example: `export MOSTRO_CONTAINER_USER=$(id -u):$(id -g)`

- `MOSTRO_DB_PASSWORD`: Not used (database encryption was removed). Kept in `compose.yml` for backward compatibility; can be omitted or left empty.

## Usage Examples
Expand Down
34 changes: 26 additions & 8 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ 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`. Both belong to the user that ran the command.

The container runs as uid/gid 1000 by default, which matches the first user account on most hosts. If yours is not uid 1000, run the container as yourself instead of handing the config directory over — the daemon also writes `mostro.db` into it:

```sh
export MOSTRO_CONTAINER_USER=$(id -u):$(id -g)
```

`compose.yml` reads that variable, so export it in the same shell you run `make docker-up` from.

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 +99,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
```

Mode `0600` on the macaroon inside a `0700` directory means only their owner can reach the file, and the container runs as uid/gid 1000 by default. If your user is not uid 1000, run the container as yourself by adding `--user $(id -u):$(id -g)` to the `docker run` command in step 4 — that also lets it write `mostro.db` into your config directory.

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 +139,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
install -d -o 1000 -g 1000 /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 both directories are handed to the container's user: it needs to write `mostro.db` into the config directory, and the `lnd` subdirectory is created owner-only because of what goes in it (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 +154,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
6 changes: 6 additions & 0 deletions docker/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ services:
dockerfile: docker/Dockerfile
volumes:
- ./config:/config # settings.toml and mostro.db
# The image's mostrouser is uid/gid 1000, so the default keeps the previous
# behaviour. Hosts whose user is not uid 1000 export
# MOSTRO_CONTAINER_USER=$(id -u):$(id -g) instead of handing ./config over
# to uid 1000 — the LND macaroon is installed 0600, and the daemon also
# needs to write mostro.db in the same directory.
user: "${MOSTRO_CONTAINER_USER:-1000:1000}"
platform: linux/amd64
networks:
- default
Expand Down
12 changes: 12 additions & 0 deletions docs/STARTUP_AND_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ 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's `other` permission bits are set, and suggests `chmod o=`. The check
is advisory — the daemon still starts — and tolerates `0640`, the mode LND
itself writes the macaroon with, so reading it through the node's group
stays supported.
- Only the mode bits are inspected: a POSIX ACL can grant a named user access
without setting them, so a quiet startup is not proof that no other account
can read the file.
- `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
151 changes: 151 additions & 0 deletions src/config/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//! 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 has any of its "other" permission bits set,
/// which puts it within reach of every account on the host.
///
/// 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)) {
// `chmod o=` rather than `chmod 600`: the file may legitimately be
// owned by the LND account and read by mostrod through the node's
// group, and following advice that drops the group bits would leave
// the daemon unable to authenticate on its next restart.
tracing::warn!(
"{label} ({path}) has permissions {mode:04o}: its \"other\" bits are set, so every \
account on this host can reach it. Clear them with: chmod o= {path}"
);
}
}

/// 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.
///
/// Only the mode bits are read. A POSIX ACL can widen access without touching
/// them, so a quiet startup means "the mode bits are sane", not "no other
/// account can read this file" — the check is a cheap guard against the
/// documented copy-it-into-place flows, not an audit.
///
/// `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");
}
}
Loading