feat: support MOSTRO_NSEC_PRIVKEY env var for Nostr private key#713
feat: support MOSTRO_NSEC_PRIVKEY env var for Nostr private key#713AndreaDiazCorreia wants to merge 3 commits intomainfrom
Conversation
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
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThe PR introduces support for optional Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| std::fs::OpenOptions::new() | ||
| .write(true) | ||
| .create(true) | ||
| .truncate(true) | ||
| .mode(0o600) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
src/config/wizard.rs (2)
208-208: Reuse the env-var name and.envfilename from shared constants.Both
"MOSTRO_NSEC_PRIVKEY"(line 225) and".env"(line 208) are duplicated here after being introduced asNSEC_ENV_VAR/ENV_FILENAMEinsrc/config/util.rs. Centralising them insrc/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.rsandwizard.rsto import fromcrate::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:
.mode(0o600)only applies when the file is created. If~/.mostro/.envalready exists with looser permissions,OpenOptions::openpreserves them — nochmodtakes effect. Callingstd::fs::set_permissionsafter opening guarantees 0o600 regardless..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 (whensettings.tomlis 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.envload fails.Silently discarding the
dotenvy::from_pathresult will hide misconfigurations (e.g., malformed.envor permission errors on the file) from operators. A singletracing::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
tracingspans 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. Usinglock().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 testsblocks 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-emptynsec_privkeyafter the env override.With
nsec_privkeynow#[serde(default)]and the env override applied here, it's possible forsettings.nostr.nsec_privkeyto be an empty string (no TOML value AND no/whitespace-only env var) wheninit_mostro_settingsruns. The failure currently surfaces later insrc/util.rs::get_keysas 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
Cargo.tomlREADME.mddocs/STARTUP_AND_CONFIG.mdsrc/config/types.rssrc/config/util.rssrc/config/wizard.rs
| 1. **`~/.mostro/.env`** (auto-loaded at startup, `chmod 600` recommended): | ||
| ``` | ||
| MOSTRO_NSEC_PRIVKEY=nsec1... | ||
| ``` |
There was a problem hiding this comment.
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.
Motivation
Storing the Nostr private key inline in
settings.tomlmakes 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_PRIVKEYenvironment variable, which is the dominant pattern across Nostr daemons (Alby Hub, NWC, OpenClaw) and follows 12-factor conventions.Changes
MOSTRO_NSEC_PRIVKEYenv var support. Precedence: env var ><settings_dir>/.env(auto-loaded viadotenvy) >settings.toml.nsec_privkeyoptional insettings.tomlvia#[serde(default)], sooperators relying on the env var can omit the field entirely.
~/.mostro/.env(recommended,chmod 600) or inline insettings.toml.whitespace-only env ignored, no env keeps TOML, trimming, TOML without the
field).
Backward compatibility
Fully backward-compatible. Existing
nsec_privkey = 'nsec1...'insettings.tomlkeeps working exactly as before. The env var only takes effect when explicitly set.Summary by CodeRabbit
New Features
MOSTRO_NSEC_PRIVKEYenvironment variable..envfile loading with environment variable precedence support..envfile (recommended) orsettings.toml.Documentation