add optional encryption to rsa_key.pem

This commit is contained in:
Andreas Piesk 2026-05-03 21:32:22 +02:00
parent f21a3adae2
commit 18a1b00aa7
3 changed files with 58 additions and 6 deletions

View file

@ -69,14 +69,59 @@ pub async fn initialize_keys() -> Result<(), Error> {
Err(e) => return Err(e.into()),
};
let (priv_key, priv_key_buffer) = if let Some(priv_key_buffer) = priv_key_buffer {
(Rsa::private_key_from_pem(priv_key_buffer.to_vec().as_slice())?, priv_key_buffer.to_vec())
let passphrase = CONFIG.rsa_key_passphrase();
let (priv_key, priv_key_buffer) = if let Some(stored_buffer) = priv_key_buffer {
let mut passphrase_error: Option<Error> = None;
let mut key_is_unencrypted = true;
// The callback is only invoked if the key file is encrypted. For unencrypted keys it is never called.
let rsa = Rsa::private_key_from_pem_callback(stored_buffer.to_vec().as_slice(), |buf| {
key_is_unencrypted = false;
if passphrase.is_empty() {
// Only reached when key is encrypted. Return any error to abort the callback;
// the actual error message is stored in passphrase_error above.
passphrase_error = Some(Error::other(
"Private RSA key is encrypted but RSA_KEY_PASSPHRASE is not configured",
));
return Err(openssl::error::ErrorStack::get());
}
let bytes = passphrase.as_bytes();
buf[..bytes.len()].copy_from_slice(bytes);
Ok(bytes.len())
});
let rsa = match rsa {
Ok(r) => r,
Err(_) if passphrase_error.is_some() => return Err(passphrase_error.unwrap().into()),
Err(e) => return Err(e.into()),
};
if key_is_unencrypted && !passphrase.is_empty() {
warn!(
"RSA key passphrase is configured but the existing private key '{}' is not encrypted.",
CONFIG.private_rsa_key()
);
}
// EncodingKey requires an unencrypted PEM, so export the in-memory key regardless of how it was stored on disk.
let unencrypted_pem = rsa.private_key_to_pem()?;
(rsa, unencrypted_pem)
} else {
let rsa_key = Rsa::generate(2048)?;
let priv_key_buffer = rsa_key.private_key_to_pem()?;
operator.write(&rsa_key_filename, priv_key_buffer.clone()).await?;
// Store encrypted on disk if a passphrase is configured, otherwise store plaintext.
let stored_pem = if passphrase.is_empty() {
rsa_key.private_key_to_pem()?
} else {
use openssl::symm::Cipher;
rsa_key.private_key_to_pem_passphrase(Cipher::aes_256_cbc(), passphrase.as_bytes())?
};
operator.write(&rsa_key_filename, stored_pem).await?;
info!("Private key '{}' created correctly", CONFIG.private_rsa_key());
(rsa_key, priv_key_buffer)
let unencrypted_pem = rsa_key.private_key_to_pem()?;
(rsa_key, unencrypted_pem)
};
let pub_key_buffer = priv_key.public_key_to_pem()?;

View file

@ -520,6 +520,8 @@ make_config! {
templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder);
/// Session JWT key
rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder);
/// RSA key passphrase |> Passphrase used to encrypt the private RSA key file on disk (leave empty for unencrypted)
rsa_key_passphrase: String, false, def, String::new();
/// Web vault folder
web_vault_folder: String, false, def, "web-vault/".to_string();
},
@ -959,6 +961,11 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",));
}
// 1024 = OpenSSL's PEM_BUFSIZE, the size of the buffer passed to the passphrase callback
if cfg.rsa_key_passphrase.as_bytes().len() > 1024 {
err!("`RSA_KEY_PASSPHRASE` must not exceed 1024 bytes");
}
if let Some(log_file) = &cfg.log_file {
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
err!("Unable to write to log file", log_file);

View file

@ -77,7 +77,7 @@ async fn main() -> Result<(), Error> {
check_data_folder().await;
auth::initialize_keys().await.unwrap_or_else(|e| {
error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key());
error!("Error creating or loading private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key());
exit(1);
});
check_web_vault();