fix(security): encrypt app secrets at rest with AES-256-GCM (#6)
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 8s
CI Pipeline / Clippy Lints (push) Successful in 50s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 8s
CI Pipeline / Clippy Lints (push) Successful in 50s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
Encrypt three sensitive secrets that were stored in plaintext: OIDC client_secret, SMTP smtp_password, TOTP totp_secret. AES-256-GCM via pm-core::crypto helper. New per-install key at /etc/patch-manager/keys/secret-encryption.key, separate from health-check.key for blast-radius isolation. MASKED placeholder behavior in API responses is preserved. 23 files changed, +1248 / -28. Closes #6.
This commit is contained in:
committed by
GitHub
parent
e0a9037be3
commit
b9fb3427e0
44
crates/pm-web/src/secret_key.rs
Normal file
44
crates/pm-web/src/secret_key.rs
Normal file
@ -0,0 +1,44 @@
|
||||
//! Secret-encryption key loader for pm-web.
|
||||
//!
|
||||
//! Lazily loads the per-install AES-256-GCM key from
|
||||
//! `/etc/patch-manager/keys/secret-encryption.key` on first use, caches it
|
||||
//! in process memory, and returns a `&'static [u8; 32]` for all subsequent calls.
|
||||
//!
|
||||
//! Uses `std::sync::OnceLock` (stable since Rust 1.70) to avoid the `once_cell` dependency.
|
||||
//!
|
||||
//! See `tasks/secret-encryption-spec.md` section 4.4 for the design rationale.
|
||||
|
||||
use pm_core::crypto;
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static SECRET_KEY: OnceLock<[u8; 32]> = OnceLock::new();
|
||||
|
||||
/// Load the secret-encryption key at first call. Subsequent calls return the cached value.
|
||||
/// Returns `CryptoError` if the key file is missing or invalid.
|
||||
///
|
||||
/// Auto-generates the key file on first start (with 0600 permissions) if it doesn't exist.
|
||||
#[allow(dead_code)]
|
||||
pub fn get() -> Result<&'static [u8; 32], crypto::CryptoError> {
|
||||
if let Some(key) = SECRET_KEY.get() {
|
||||
return Ok(key);
|
||||
}
|
||||
let key = crypto::load_or_create_key(Path::new(crypto::SECRET_ENCRYPTION_KEY_PATH))?;
|
||||
// _ = ignore error if another thread won the race (already set by them)
|
||||
let _ = SECRET_KEY.set(key);
|
||||
Ok(SECRET_KEY.get().expect("key was just set"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[test]
|
||||
fn once_lock_caches_value() {
|
||||
let cell: OnceLock<u32> = OnceLock::new();
|
||||
let v1 = cell.get_or_init(|| 42);
|
||||
let v2 = cell.get_or_init(|| 99); // Should return 42, not 99
|
||||
assert_eq!(*v1, 42);
|
||||
assert_eq!(*v2, 42);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user