Skip to content

feat: support MOSTRO_NSEC_PRIVKEY env var for Nostr private key#713

Open
AndreaDiazCorreia wants to merge 3 commits intomainfrom
feat/nsec-env-var
Open

feat: support MOSTRO_NSEC_PRIVKEY env var for Nostr private key#713
AndreaDiazCorreia wants to merge 3 commits intomainfrom
feat/nsec-env-var

Conversation

@AndreaDiazCorreia
Copy link
Copy Markdown
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Apr 24, 2026

Motivation

Storing the Nostr private key inline in settings.toml makes it easy to leak it accidentally: config files end up in logs, backups, bug reports, and chat messages when operators ask for help. It also makes integration with Docker secrets, systemd credentials, and similar tools harder than it should be.

This PR lets operators provide the nsec via the MOSTRO_NSEC_PRIVKEY environment variable, which is the dominant pattern across Nostr daemons (Alby Hub, NWC, OpenClaw) and follows 12-factor conventions.

Changes

  • Add MOSTRO_NSEC_PRIVKEY env var support. Precedence: env var >
    <settings_dir>/.env (auto-loaded via dotenvy) > settings.toml.
  • Make nsec_privkey optional in settings.toml via #[serde(default)], so
    operators relying on the env var can omit the field entirely.
  • Update the setup wizard to ask where to store the generated key: either in
    ~/.mostro/.env (recommended, chmod 600) or inline in settings.toml.
  • Documentation updated with examples for shell, systemd, and Docker.
  • Unit tests for all precedence cases (env wins, empty env falls back,
    whitespace-only env ignored, no env keeps TOML, trimming, TOML without the
    field).

Backward compatibility

Fully backward-compatible. Existing nsec_privkey = 'nsec1...' in settings.toml keeps working exactly as before. The env var only takes effect when explicitly set.

Summary by CodeRabbit

  • New Features

    • Nostr private key can now be configured via MOSTRO_NSEC_PRIVKEY environment variable.
    • Automatic .env file loading with environment variable precedence support.
    • Configuration wizard now offers storing private key in .env file (recommended) or settings.toml.
  • Documentation

    • Updated configuration guides with environment variable override details and precedence order.

Add MOSTRO_NSEC_PRIVKEY environment variable to allow storing the Nostr private key separately from settings.toml. The variable can be set via real environment, ~/.mostro/.env (auto-loaded at startup), or systemd/Docker configuration.

Changes:
- Add dotenvy dependency for .env file loading
- Implement load_env_file() to auto-load <settings_dir>/.env at startup
- Implement apply_nsec_env_override() to read MOSTRO_NSEC_PRIVKEY and override
…ent variable

- Mark NostrSettings.nsec_privkey as optional with #[serde(default)]
- Remove "I'll configure MOSTRO_NSEC_PRIVKEY myself" option from setup wizard
- Export MOSTRO_NSEC_PRIVKEY to current process when saving to .env so daemon can use it immediately without restart
- Add test verifying settings.toml parses without nsec_privkey field when relying on environment variable
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@AndreaDiazCorreia has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4922afc9-570b-4398-bbe4-271cf048d31f

📥 Commits

Reviewing files that changed from the base of the PR and between dd9e3e3 and b9565bf.

📒 Files selected for processing (1)
  • src/config/wizard.rs

Walkthrough

The PR introduces support for optional nsec_privkey configuration through environment variables and .env files. It adds the dotenvy dependency, makes the field optional with #[serde(default)], implements environment variable loading with precedence handling (env var > .env file > settings.toml), and updates the wizard to prompt users where to store the private key.

Changes

Cohort / File(s) Summary
Dependency
Cargo.toml
Adds dotenvy crate (v0.15.7) for .env file loading.
Documentation
README.md, docs/STARTUP_AND_CONFIG.md
Clarifies nsec_privkey is optional, documents environment variable override (MOSTRO_NSEC_PRIVKEY), .env file precedence, and deployment instructions across systemd, Docker, and local environments.
Configuration System
src/config/types.rs, src/config/util.rs
Makes nsec_privkey optional via #[serde(default)], implements .env loading and environment-driven override with precedence handling; adds unit tests for override, trimming, and fallback behavior.
Interactive Setup
src/config/wizard.rs
Updates wizard to prompt for nsec storage location (.env file with restricted permissions or settings.toml), implements conditional storage flow, and adds write_env_file helper for secure file writes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • arkanoider
  • grunch

Poem

🐰 hops with delight
Secrets now have safer ground,
In .env files, snugly bound,
Env vars take the highest place,
Settings.toml as backup space,
Mostro's keys hop with better grace! 🗝️

🚥 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 title clearly summarizes the main change: adding support for MOSTRO_NSEC_PRIVKEY environment variable for Nostr private key configuration. It is concise, specific, and directly reflects the primary purpose of the changeset across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nsec-env-var

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 and usage tips.

Copy link
Copy Markdown

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

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: dd9e3e33be

ℹ️ 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/config/wizard.rs
Comment on lines +251 to +255
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce 0600 when rewriting an existing .env file

On Unix, OpenOptionsExt::mode(0o600) only applies when the file is newly created; it does not tighten permissions on an already-existing file. If .env already exists with broader bits (for example 0644), this path truncates and rewrites it but leaves those broad permissions in place, exposing MOSTRO_NSEC_PRIVKEY to other local users. After opening/writing, explicitly set permissions to 0o600 (or recreate the file) so the secret is always protected.

Useful? React with 👍 / 👎.

Comment thread src/config/util.rs
fn load_env_file(settings_dir: &std::path::Path) {
let env_file = settings_dir.join(ENV_FILENAME);
if env_file.exists() {
let _ = dotenvy::from_path(&env_file);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Report .env parsing/loading failures

This ignores any error from dotenvy::from_path, so malformed or unreadable .env files fail silently. In deployments that rely on MOSTRO_NSEC_PRIVKEY from .env, startup can then proceed with the TOML value (or an empty key) and either use the wrong identity or fail later with an unrelated parse error, making the root cause hard to diagnose. Propagating or logging this error would prevent silent misconfiguration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/config/wizard.rs (2)

208-208: Reuse the env-var name and .env filename from shared constants.

Both "MOSTRO_NSEC_PRIVKEY" (line 225) and ".env" (line 208) are duplicated here after being introduced as NSEC_ENV_VAR / ENV_FILENAME in src/config/util.rs. Centralising them in src/config/constants.rs (and re-using from both modules) prevents drift if the name ever changes.

♻️ Suggested approach

Promote them in src/config/constants.rs:

pub const ENV_FILENAME: &str = ".env";
pub const NSEC_ENV_VAR: &str = "MOSTRO_NSEC_PRIVKEY";

Then update util.rs and wizard.rs to import from crate::config::constants::{ENV_FILENAME, NSEC_ENV_VAR} instead of defining/hardcoding them locally.

-    let env_file_path = settings_dir.join(".env");
+    let env_file_path = settings_dir.join(ENV_FILENAME);
@@
-        std::env::set_var("MOSTRO_NSEC_PRIVKEY", nsec);
+        std::env::set_var(NSEC_ENV_VAR, nsec);
@@
-    writeln!(file, "MOSTRO_NSEC_PRIVKEY={}", nsec)
+    writeln!(file, "{}={}", NSEC_ENV_VAR, nsec)

Also applies to: 225-225

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/wizard.rs` at line 208, Replace the hardcoded ".env" and
"MOSTRO_NSEC_PRIVKEY" occurrences with the shared constants ENV_FILENAME and
NSEC_ENV_VAR from crate::config::constants: update wizard.rs (where
env_file_path is built and where the NSEC env var is referenced) and util.rs to
import crate::config::constants::{ENV_FILENAME, NSEC_ENV_VAR} instead of
defining or hardcoding these values locally, and remove the duplicate local
definitions so both modules reuse the single source of truth.

247-270: write_env_file: permission + truncation edge cases.

Two small robustness gaps in the env-file writer:

  1. .mode(0o600) only applies when the file is created. If ~/.mostro/.env already exists with looser permissions, OpenOptions::open preserves them — no chmod takes effect. Calling std::fs::set_permissions after opening guarantees 0o600 regardless.
  2. .truncate(true) discards any unrelated entries the operator might have added to .env. In the current flow this is called only during initial wizard setup (when settings.toml is being created), so collisions are unlikely, but an append-or-replace strategy would be safer long-term.
🛡️ Proposed hardening for (1)
     let mut file = file.map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt;
+        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
+            .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
+    }
     writeln!(file, "MOSTRO_NSEC_PRIVKEY={}", nsec)
         .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/wizard.rs` around lines 247 - 270, In write_env_file, ensure file
permissions are enforced after opening by calling std::fs::set_permissions(path,
Permissions::from_mode(0o600)) (or the cross-platform equivalent) so existing
files get 0o600 even when not created; and avoid blind truncate by implementing
an "append-or-replace" flow: if the file exists, read its contents, replace any
line starting with "MOSTRO_NSEC_PRIVKEY=" with the new value (or append the line
if missing), then write the updated content back (you may truncate when writing
the final assembled content). Keep these changes localized to the write_env_file
function so it still uses OpenOptions for creation but then enforces permissions
and safely updates the MOSTRO_NSEC_PRIVKEY entry.
src/config/util.rs (3)

21-26: Consider logging when .env load fails.

Silently discarding the dotenvy::from_path result will hide misconfigurations (e.g., malformed .env or permission errors on the file) from operators. A single tracing::warn! keeps behavior backward-compatible but makes troubleshooting easier.

🔎 Proposed fix
 fn load_env_file(settings_dir: &std::path::Path) {
     let env_file = settings_dir.join(ENV_FILENAME);
     if env_file.exists() {
-        let _ = dotenvy::from_path(&env_file);
+        if let Err(err) = dotenvy::from_path(&env_file) {
+            tracing::warn!(
+                path = %env_file.display(),
+                error = %err,
+                "Failed to load .env file"
+            );
+        }
     }
 }

As per coding guidelines: "Prefer tracing spans over ad-hoc logging in Rust code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/util.rs` around lines 21 - 26, The function load_env_file
currently discards the result of dotenvy::from_path, hiding failures; update
load_env_file to inspect the Result from dotenvy::from_path(&env_file) and, on
Err, emit a tracing::warn! (including the error and the env_file path/metadata)
so operators see malformed or permission errors while keeping behavior otherwise
the same; reference the existing load_env_file function and the call to
dotenvy::from_path to locate where to add the Result handling and tracing::warn!
(prefer tracing spans if you already have one in scope, otherwise a simple
tracing::warn! with the error and file path is fine).

138-261: Test suite LGTM; one optional hardening note.

Tests comprehensively cover precedence (env > toml), blank/whitespace fallback, trimming, and missing-field TOML parsing. One small resilience nit: ENV_LOCK.lock().unwrap() will poison the mutex if any test in the critical section panics, cascading failures into subsequent tests. Using lock().unwrap_or_else(|e| e.into_inner()) makes the suite more robust to a single failure.

As per coding guidelines: "Co-locate tests in their modules under mod tests blocks with descriptive names" — the descriptive test names here comply.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/util.rs` around lines 138 - 261, Tests currently call
ENV_LOCK.lock().unwrap(), which will poison the mutex and cause cascading
failures if any test panics; change those calls to use lock().unwrap_or_else(|e|
e.into_inner()) so the guard is recovered instead of panicking. Update the five
test functions that create the lock (_lock in env_var_overrides_toml_nsec,
empty_env_var_falls_back_to_toml, no_env_var_keeps_toml,
whitespace_only_env_is_ignored, env_var_value_is_trimmed) to acquire the mutex
with ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()).

40-60: Validate non-empty nsec_privkey after the env override.

With nsec_privkey now #[serde(default)] and the env override applied here, it's possible for settings.nostr.nsec_privkey to be an empty string (no TOML value AND no/whitespace-only env var) when init_mostro_settings runs. The failure currently surfaces later in src/util.rs::get_keys as a generic "Failed to parse nostr private key" message. A startup-time check gives operators a clearer, more actionable error and fails fast.

🛡️ Proposed fix
 /// Validates Mostro settings on startup
 fn validate_mostro_settings(settings: &Settings) -> Result<(), MostroError> {
     let dev_fee = settings.mostro.dev_fee_percentage;
@@
+    if settings.nostr.nsec_privkey.trim().is_empty() {
+        return Err(MostroInternalErr(ServiceError::IOError(format!(
+            "Nostr private key is missing. Set `{}` in the environment (or `{}/.env`), or set `nsec_privkey` in settings.toml.",
+            NSEC_ENV_VAR, ENV_FILENAME
+        ))));
+    }
+
     Ok(())
 }

Also applies to: 105-106, 122-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/util.rs` around lines 40 - 60, Add a startup validation in
validate_mostro_settings to ensure settings.nostr.nsec_privkey is not empty or
whitespace after env overrides: check
settings.nostr.nsec_privkey.trim().is_empty() and return an immediate
MostroInternalErr/ServiceError with a clear message like "nostr private key
(nsec_privkey) is empty after env override" so init_mostro_settings fails fast
(this prevents the later generic error in get_keys).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 523-526: The fenced code block in README.md showing the
MOSTRO_NSEC_PRIVKEY example is missing a language specifier and triggers
markdownlint MD040; update the triple-backtick that opens the block to include a
language (e.g., "ini") so the block becomes ```ini and thus documents syntax and
satisfies MD040—look for the snippet containing MOSTRO_NSEC_PRIVKEY and change
the opening fence accordingly.

In `@src/config/wizard.rs`:
- Around line 200-206: The formatting mismatch comes from the first println!
splitting a single string literal across multiple lines; update that println!
(the one starting with "Storing your nsec as an environment variable (instead of
settings.toml) keeps") so it uses a single contiguous string literal like the
subsequent println! calls (i.e., combine the wrapped lines into one println!
call with the full message) so rustfmt/cargo fmt will no longer reflow it and CI
will pass.

---

Nitpick comments:
In `@src/config/util.rs`:
- Around line 21-26: The function load_env_file currently discards the result of
dotenvy::from_path, hiding failures; update load_env_file to inspect the Result
from dotenvy::from_path(&env_file) and, on Err, emit a tracing::warn! (including
the error and the env_file path/metadata) so operators see malformed or
permission errors while keeping behavior otherwise the same; reference the
existing load_env_file function and the call to dotenvy::from_path to locate
where to add the Result handling and tracing::warn! (prefer tracing spans if you
already have one in scope, otherwise a simple tracing::warn! with the error and
file path is fine).
- Around line 138-261: Tests currently call ENV_LOCK.lock().unwrap(), which will
poison the mutex and cause cascading failures if any test panics; change those
calls to use lock().unwrap_or_else(|e| e.into_inner()) so the guard is recovered
instead of panicking. Update the five test functions that create the lock (_lock
in env_var_overrides_toml_nsec, empty_env_var_falls_back_to_toml,
no_env_var_keeps_toml, whitespace_only_env_is_ignored, env_var_value_is_trimmed)
to acquire the mutex with ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()).
- Around line 40-60: Add a startup validation in validate_mostro_settings to
ensure settings.nostr.nsec_privkey is not empty or whitespace after env
overrides: check settings.nostr.nsec_privkey.trim().is_empty() and return an
immediate MostroInternalErr/ServiceError with a clear message like "nostr
private key (nsec_privkey) is empty after env override" so init_mostro_settings
fails fast (this prevents the later generic error in get_keys).

In `@src/config/wizard.rs`:
- Line 208: Replace the hardcoded ".env" and "MOSTRO_NSEC_PRIVKEY" occurrences
with the shared constants ENV_FILENAME and NSEC_ENV_VAR from
crate::config::constants: update wizard.rs (where env_file_path is built and
where the NSEC env var is referenced) and util.rs to import
crate::config::constants::{ENV_FILENAME, NSEC_ENV_VAR} instead of defining or
hardcoding these values locally, and remove the duplicate local definitions so
both modules reuse the single source of truth.
- Around line 247-270: In write_env_file, ensure file permissions are enforced
after opening by calling std::fs::set_permissions(path,
Permissions::from_mode(0o600)) (or the cross-platform equivalent) so existing
files get 0o600 even when not created; and avoid blind truncate by implementing
an "append-or-replace" flow: if the file exists, read its contents, replace any
line starting with "MOSTRO_NSEC_PRIVKEY=" with the new value (or append the line
if missing), then write the updated content back (you may truncate when writing
the final assembled content). Keep these changes localized to the write_env_file
function so it still uses OpenOptions for creation but then enforces permissions
and safely updates the MOSTRO_NSEC_PRIVKEY entry.
🪄 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: b7d2c87c-cdb5-431e-9b65-65c80b70aad1

📥 Commits

Reviewing files that changed from the base of the PR and between 673340d and dd9e3e3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • Cargo.toml
  • README.md
  • docs/STARTUP_AND_CONFIG.md
  • src/config/types.rs
  • src/config/util.rs
  • src/config/wizard.rs

Comment thread README.md
Comment on lines +523 to +526
1. **`~/.mostro/.env`** (auto-loaded at startup, `chmod 600` recommended):
```
MOSTRO_NSEC_PRIVKEY=nsec1...
```
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.

⚠️ Potential issue | 🟡 Minor

Add a language specifier to the new fenced code block (MD040).

The .env example opens with a bare triple-backtick, which violates markdownlint MD040 (also flagged by static analysis on line 524).

📝 Proposed fix
 1. **`~/.mostro/.env`** (auto-loaded at startup, `chmod 600` recommended):
-   ```
+   ```ini
    MOSTRO_NSEC_PRIVKEY=nsec1...
    ```

As per coding guidelines: "Add a language specifier to every fenced code block in documentation to comply with markdownlint MD040".

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 524-524: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 523 - 526, The fenced code block in README.md showing
the MOSTRO_NSEC_PRIVKEY example is missing a language specifier and triggers
markdownlint MD040; update the triple-backtick that opens the block to include a
language (e.g., "ini") so the block becomes ```ini and thus documents syntax and
satisfies MD040—look for the snippet containing MOSTRO_NSEC_PRIVKEY and change
the opening fence accordingly.

Comment thread src/config/wizard.rs Outdated
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.

1 participant