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
38
crates/pm-worker/src/email.rs
Executable file → Normal file
38
crates/pm-worker/src/email.rs
Executable file → Normal file
@ -32,11 +32,16 @@ struct NotificationSettings {
|
||||
}
|
||||
|
||||
/// Load SMTP settings from the `system_config` table.
|
||||
///
|
||||
/// Issue #6 fix: SMTP password is stored as two rows:
|
||||
/// - `smtp_password_encrypted` (hex of AES-256-GCM ciphertext)
|
||||
/// - `smtp_password_nonce` (hex of AES-256-GCM nonce)
|
||||
async fn load_smtp_settings(pool: &PgPool) -> SmtpSettings {
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT key, value FROM system_config WHERE key IN (
|
||||
'smtp_enabled', 'smtp_host', 'smtp_port', 'smtp_username',
|
||||
'smtp_password', 'smtp_from', 'smtp_tls_mode'
|
||||
'smtp_password_encrypted', 'smtp_password_nonce',
|
||||
'smtp_from', 'smtp_tls_mode'
|
||||
)",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@ -50,17 +55,46 @@ async fn load_smtp_settings(pool: &PgPool) -> SmtpSettings {
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
// Decrypt the SMTP password
|
||||
let enc_hex = get("smtp_password_encrypted");
|
||||
let nonce_hex = get("smtp_password_nonce");
|
||||
let password = if !enc_hex.is_empty() && !nonce_hex.is_empty() {
|
||||
match (
|
||||
hex_decode(&enc_hex),
|
||||
hex_decode(&nonce_hex),
|
||||
crate::secret_key::get(),
|
||||
) {
|
||||
(Some(enc), Some(nonce), Ok(key)) => {
|
||||
pm_core::crypto::decrypt(&enc, &nonce, key).unwrap_or_default()
|
||||
},
|
||||
_ => String::new(),
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
SmtpSettings {
|
||||
enabled: get("smtp_enabled") == "true",
|
||||
host: get("smtp_host"),
|
||||
port: get("smtp_port").parse().unwrap_or(587),
|
||||
username: get("smtp_username"),
|
||||
password: get("smtp_password"),
|
||||
password,
|
||||
from: get("smtp_from"),
|
||||
tls_mode: get("smtp_tls_mode"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a hex string to bytes. Returns None on invalid input.
|
||||
fn hex_decode(s: &str) -> Option<Vec<u8>> {
|
||||
if !s.len().is_multiple_of(2) {
|
||||
return None;
|
||||
}
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Load notification preferences from `system_config`.
|
||||
async fn load_notification_settings(pool: &PgPool) -> NotificationSettings {
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(
|
||||
|
||||
Reference in New Issue
Block a user