rws_server/secrets/mod.rs
1//! Secret-reference resolution for HashiCorp Vault, AWS Secrets Manager, and
2//! Azure Key Vault — no vendor SDK for any of the three, matching this
3//! crate's existing `storage-s3`/`storage-azure`/`sso` modules.
4//!
5//! JWT signing keys, DB credentials, and TLS keys are ordinarily just plain
6//! `RWS_CONFIG_*`/application env vars. [`resolve`] lets any of those values
7//! instead be a *reference* to a secret held in a managed secrets store:
8//!
9//! | Prefix | Backend | Example |
10//! |---|---|---|
11//! | `vault://path#field` | HashiCorp Vault (KV v2) | `vault://secret/myapp/db#password` |
12//! | `aws-sm://name` or `aws-sm://name#field` | AWS Secrets Manager | `aws-sm://prod/db-password` |
13//! | `azkv://vault-name/secret-name` | Azure Key Vault | `azkv://my-kv/db-password` |
14//!
15//! A value that doesn't start with one of these prefixes is returned
16//! unchanged — resolution is purely additive, not a new required step for
17//! every existing plain-value config var.
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! # extern crate rws_server as rust_web_server;
23//! use rust_web_server::secrets;
24//!
25//! // VAULT_ADDR / VAULT_TOKEN (Vault), AWS_REGION + credentials (Secrets
26//! // Manager), or AZURE_KEY_VAULT_TENANT_ID + co. (Key Vault) must already
27//! // be set in the environment for the corresponding prefix to resolve.
28//! let db_password = secrets::resolve("vault://secret/myapp/db#password")?;
29//! # Ok::<(), secrets::SecretsError>(())
30//! ```
31//!
32//! # Automatic env var resolution
33//!
34//! [`resolve_env_vars`] scans every currently-set `RWS_`-prefixed environment
35//! variable and rewrites in place (via `std::env::set_var`) any whose value
36//! matches one of the prefixes above. `Server::setup()` calls this
37//! automatically (right after `entry_point::bootstrap()`, so it sees the
38//! fully layered config) once this feature is enabled, so **any**
39//! `RWS_CONFIG_*` value — `RWS_CONFIG_TLS_KEY_FILE`, a config-driven proxy's
40//! `token_env`-referenced variable, anything — can be a secret reference
41//! with no code changes, matching the "additive to every existing
42//! `RWS_CONFIG_*`/`RWS_*` env var" goal this feature was built for.
43//!
44//! # Failure mode: fail fast, not fail open
45//!
46//! A value that *looks* like a secret reference but fails to resolve (wrong
47//! token, unreachable backend, missing field, ...) is a startup error, not a
48//! silently-ignored one — the alternative would be a server starting up with
49//! a JWT signing key or DB password literally equal to the string
50//! `"vault://secret/myapp/db#password"`, which is worse than refusing to
51//! start at all.
52
53#[cfg(test)]
54mod tests;
55
56mod aws_secrets_manager;
57mod azure_key_vault;
58mod vault;
59
60use std::fmt;
61
62/// Error resolving a secret reference — a network failure, an
63/// authentication failure, a missing field, or a malformed reference.
64#[derive(Debug)]
65pub struct SecretsError(pub String);
66
67impl SecretsError {
68 pub fn new(msg: impl Into<String>) -> Self {
69 SecretsError(msg.into())
70 }
71}
72
73impl fmt::Display for SecretsError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.write_str(&self.0)
76 }
77}
78
79impl std::error::Error for SecretsError {}
80
81impl From<crate::storage::StorageError> for SecretsError {
82 fn from(e: crate::storage::StorageError) -> Self {
83 SecretsError(e.0)
84 }
85}
86
87const VAULT_PREFIX: &str = "vault://";
88const AWS_SM_PREFIX: &str = "aws-sm://";
89const AZURE_KV_PREFIX: &str = "azkv://";
90
91/// Resolves `value` if it starts with a recognized secret-reference prefix
92/// (`vault://`, `aws-sm://`, `azkv://`); otherwise returns it unchanged.
93///
94/// See the [module docs](self) for the prefix formats and each backend's
95/// required environment variables.
96pub fn resolve(value: &str) -> Result<String, SecretsError> {
97 if let Some(rest) = value.strip_prefix(VAULT_PREFIX) {
98 return vault::resolve(rest);
99 }
100 if let Some(rest) = value.strip_prefix(AWS_SM_PREFIX) {
101 return aws_secrets_manager::resolve(rest);
102 }
103 if let Some(rest) = value.strip_prefix(AZURE_KV_PREFIX) {
104 return azure_key_vault::resolve(rest);
105 }
106 Ok(value.to_string())
107}
108
109fn is_secret_ref(value: &str) -> bool {
110 value.starts_with(VAULT_PREFIX) || value.starts_with(AWS_SM_PREFIX) || value.starts_with(AZURE_KV_PREFIX)
111}
112
113/// Scans every environment variable whose name starts with `RWS_` and
114/// rewrites in place (via `std::env::set_var`) any whose *value* matches a
115/// recognized secret-reference prefix. Variables that don't match are left
116/// untouched — this only ever narrows down to the ones that opted in by
117/// using one of the prefixes.
118///
119/// Called automatically from `entry_point::bootstrap()` when this feature is
120/// enabled; exposed publicly so a library user driving their own startup
121/// sequence (not calling `bootstrap()`) can call it explicitly instead.
122pub fn resolve_env_vars() -> Result<(), SecretsError> {
123 let matching: Vec<(String, String)> = std::env::vars()
124 .filter(|(key, value)| key.starts_with("RWS_") && is_secret_ref(value))
125 .collect();
126
127 for (key, value) in matching {
128 let resolved = resolve(&value)
129 .map_err(|e| SecretsError::new(format!("resolving secret reference for {key}: {e}")))?;
130 std::env::set_var(&key, resolved);
131 crate::server_config::invalidate();
132 }
133 Ok(())
134}