Skip to content

fix(security): harden permissions for local mnemonic database#180

Merged
grunch merged 2 commits into
mainfrom
fix/harden-mnemonic-db-permissions
Jun 23, 2026
Merged

fix(security): harden permissions for local mnemonic database#180
grunch merged 2 commits into
mainfrom
fix/harden-mnemonic-db-permissions

Conversation

@grunch

@grunch grunch commented Jun 23, 2026

Copy link
Copy Markdown
Member

Closes #179

Summary

mostro-cli stores the mnemonic that derives the user's Mostro identity and trade keys in ~/.mcli/mcli.db. Until now the directory and DB file inherited the process umask, so under a permissive umask they could be left world-readable, exposing the mnemonic to other local users. A related side-effect bug meant a command run with missing config could create that sensitive database before failing.

Changes

  • Private directory (0700)~/.mcli is created with mode 0700. get_mcli_path now delegates to a new ensure_private_dir, which also re-tightens a pre-existing directory so installs created by an older version (or a loose umask) get hardened on the next run.
  • Private DB file (0600)mcli.db is created atomically with create_new + mode(0o600), so there is never a window where the file exists world-readable. Existing databases are defensively re-tightened to 0600 on each run.
  • Validate config before creating secretsMOSTRO_PUBKEY, RELAYS and (for admin commands) ADMIN_NSEC are now validated in init_context before connect() runs, so a mistaken command with missing config fails without ever writing the mnemonic to disk.
  • No more panic on missing RELAYSconnect_nostr returns a proper error instead of expect-panicking.
  • Regression tests — cover 0700/0600 permissions, idempotent + tightening directory creation, and create_new semantics (won't truncate an existing DB).

All changes are #[cfg(unix)]-gated for the permission bits, with a plain-create fallback on non-Unix platforms.

Verification

Reproduced the issue's scenarios against the built binary with a temporary HOME:

Scenario Result
umask 000 + full config ~/.mclidrwx------ (0700), mcli.db-rw------- (0600)
Missing MOSTRO_PUBKEY Fails with guidance; no ~/.mcli or DB created
Missing RELAYS Fails with guidance; no DB created
Pre-existing loose perms (0755/0644) Re-tightened to 0700/0600 on next run

cargo test, cargo fmt --check and cargo clippy --all-targets all pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Added early validation of required runtime configuration (including clearer guidance when relay settings are missing).
    • Startup no longer panics for invalid/missing relay settings; relay parsing is more robust (trims, filters empties).
    • Existing databases/directories are now hardened, and failures to restrict permissions will stop startup.
  • New Features

    • SQLite database files and the private .mcli directory are created with owner-only permissions for improved security.
  • Tests

    • Added coverage for private directory creation and permission enforcement.

The mnemonic stored in ~/.mcli/mcli.db derives the user's Mostro
identity and trade keys. Until now both the directory and the database
file inherited the process umask, so under a permissive umask they could
be left world-readable, exposing the mnemonic to other local users.

Additionally, `connect()` (which creates the DB and generates the
mnemonic on first run) ran before required config was validated, so a
command run with missing MOSTRO_PUBKEY/RELAYS could silently create the
sensitive database before failing.

Changes (issue #179):
- Create ~/.mcli with mode 0700 and mcli.db with mode 0600. The DB file
  is created atomically via create_new + mode, avoiding any window where
  it exists world-readable.
- Defensively re-tighten an existing directory/DB on every run, so
  installs created by older versions (or a loose umask) get hardened.
- Validate MOSTRO_PUBKEY, RELAYS and (for admin commands) ADMIN_NSEC
  before calling connect(), so missing config fails without creating any
  mnemonic material on disk.
- connect_nostr returns an error instead of panicking when RELAYS is
  unset.
- Add regression tests for 0700/0600 permissions, idempotent/tightening
  directory creation, and create_new semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XW5NGXu1jYKQGgr7xqMs8c
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02c1d4e3-9593-48cd-b077-2996b167a264

📥 Commits

Reviewing files that changed from the base of the PR and between f22c331 and 100b8f9.

📒 Files selected for processing (4)
  • src/db.rs
  • src/util/misc.rs
  • src/util/net.rs
  • tests/util_misc.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/util/misc.rs
  • tests/util_misc.rs
  • src/db.rs

Walkthrough

The PR moves RELAYS validation ahead of database initialization, replaces panic-based relay loading with error propagation, and hardens .mcli directory and mcli.db file permissions on Unix with owner-only access, backed by regression tests.

Changes

Security hardening

Layer / File(s) Summary
Fail-fast startup validation
src/cli.rs, src/util/net.rs
init_context now validates RELAYS before SQLite setup and DB-backed key loading. Relay lookup in the Nostr connection path returns an error instead of panicking, with parsing that trims whitespace and rejects empty entries.
Private directory utilities
src/util/misc.rs, src/util/mod.rs, tests/util_misc.rs
New ensure_private_dir helper creates or re-tightens the local client directory to 0700 on Unix. get_mcli_path uses it, the helper is re-exported, and tests cover creation, permission tightening, idempotency, and file-vs-directory validation.
Private database file creation
src/db.rs
SQLite setup now creates the database file with owner-only 0600 permissions via a new create_private_db_file helper. Existing files have 0600 re-applied on Unix, with failures now raising errors. Tests verify file modes and create_new semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • MostroP2P/mostro-cli#149: Also changes src/cli.rs around init_context and admin-related context key handling.
  • MostroP2P/mostro-cli#152: Both PRs modify init_context startup flow to resolve MOSTRO_PUBKEY via safer, non-panicking environment handling.

Suggested reviewers

  • arkanoider
  • Catrya

Poem

🐇 I tucked the burrow tight tonight,
with doors at 0700 just right.
The relay check now guards the gate,
before the secret seeds create.
And little db, snug out of sight,
now sleeps in 0600 moonlight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main security-focused change: hardening permissions for the local mnemonic database file.
Linked Issues check ✅ Passed All primary objectives from issue #179 are met: directory (0700) and file (0600) permission hardening, pre-validation of required config before database creation, and improved error handling for missing RELAYS.
Out of Scope Changes check ✅ Passed All changes directly support the security hardening objectives; no out-of-scope modifications detected. Changes focus on permission enforcement, config validation, and error handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/harden-mnemonic-db-permissions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f22c331c2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/db.rs`:
- Around line 73-79: The code currently logs a warning and continues execution
when the set_mode call fails to restrict database permissions to 0o600, which is
a security risk. Instead of using eprintln to warn and continuing, convert this
to return the error so the program fails closed if database permissions cannot
be properly tightened. Replace the if let Err(e) block that calls eprintln with
error handling that returns the error from set_mode, ensuring the database is
not used if its permissions remain insecure.
- Around line 97-110: The create_private_db_file function has inconsistent
behavior across platforms: Unix uses create_new(true) to fail on existing files,
while non-Unix uses File::create which silently truncates existing files. Move
the create_new(true) call outside of the platform-specific conditionals so it
applies universally across all platforms, keeping only the Unix-specific
mode(0o600) call within the #[cfg(unix)] block. This ensures consistent
cross-platform behavior where creating an existing file always fails rather than
silently truncating it.

In `@src/util/misc.rs`:
- Around line 28-38: The ensure_private_dir function currently returns success
when create_dir_private fails with AlreadyExists without verifying that the
existing path is actually a directory. If the path exists as a regular file, it
will still attempt to set_mode and return Ok(()). Add metadata checking in the
AlreadyExists error branch to verify that the path is indeed a directory using
std::fs::metadata or similar, and return an appropriate error (such as
InvalidInput with a message indicating the path exists but is not a directory)
if it is not a directory before proceeding to the set_mode call.

In `@src/util/net.rs`:
- Around line 17-18: The RELAYS environment variable is split by comma, but
individual entries are not trimmed of whitespace or filtered for empty values,
which can cause failures in add_relay. After splitting the relays string by
comma, chain additional operations to trim whitespace from each entry using a
map operation and filter out any empty strings using filter before collecting
into the Vec. This ensures only valid, non-empty relay entries are processed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0322dc5-f862-4b38-837a-bf68f51467ee

📥 Commits

Reviewing files that changed from the base of the PR and between 09855f6 and f22c331.

📒 Files selected for processing (6)
  • src/cli.rs
  • src/db.rs
  • src/util/misc.rs
  • src/util/mod.rs
  • src/util/net.rs
  • tests/util_misc.rs

Comment thread src/db.rs Outdated
Comment thread src/db.rs Outdated
Comment thread src/util/misc.rs
Comment thread src/util/net.rs Outdated
- db.rs: use create_new on all platforms in create_private_db_file so the
  non-Unix fallback no longer truncates an existing database; only the
  0600 mode bit stays Unix-gated.
- db.rs: fail closed when an existing mnemonic DB can't be re-tightened to
  0600 instead of only warning, so we never keep using a world-readable DB.
- misc.rs: ensure_private_dir now verifies the pre-existing path is a
  directory before chmod-ing it and reporting success.
- net.rs: trim relay entries and drop empty ones so stray whitespace or
  trailing commas don't reach add_relay.
- tests: cover ensure_private_dir rejecting a non-directory path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XW5NGXu1jYKQGgr7xqMs8c
@grunch grunch merged commit 51a80cd into main Jun 23, 2026
6 checks passed
@grunch grunch deleted the fix/harden-mnemonic-db-permissions branch June 23, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden permissions for local mnemonic database

1 participant