Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions crates/credential-driver-sql/src/fernet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,38 @@ impl FernetKeyRepository {
})
}

/// Startup-time Null Key check (ADR 0019 §4, Security).
///
/// Unlike [`Self::load`], this does not require any key files to be
/// present — an unconfigured repository (before `credential_setup` has
/// run) is not itself a Null Key problem. It only inspects whatever key
/// files already exist and, for any that decode to the well-known Null
/// Key, emits a hard warning log and — unless `insecure_allow_null_key`
/// is set — returns an error so the caller can refuse to start the
/// service.
///
/// # Errors
/// [`CredentialFernetError::NullKeyDetected`] if a key file decodes to
/// the Null Key and `insecure_allow_null_key` is `false`.
pub fn check_startup_null_key(
&self,
insecure_allow_null_key: bool,
) -> Result<(), CredentialFernetError> {
for (idx, raw) in self.read_key_files()? {
if Self::is_null_key(&raw) {
tracing::error!(
key_index = idx,
insecure_allow_null_key,
"credential key repository contains the well-known Null Key at startup"
);
if !insecure_allow_null_key {
return Err(CredentialFernetError::NullKeyDetected);
}
}
}
Ok(())
}

/// Promote the staged key `0` to primary, stage a fresh key `0`, and
/// prune beyond [`MAX_ACTIVE_KEYS`].
///
Expand Down Expand Up @@ -348,6 +380,38 @@ mod tests {
assert!(repo.load(true).is_ok());
}

#[test]
fn test_check_startup_null_key_on_unconfigured_repository() {
let dir = tempfile::tempdir().unwrap();
let repo = FernetKeyRepository::new(dir.path().join("does-not-exist"));
// No key files at all (repository not yet set up) is not itself a
// Null Key problem.
assert!(repo.check_startup_null_key(false).is_ok());
}

#[test]
fn test_check_startup_null_key_refuses_by_default() {
let dir = tempfile::tempdir().unwrap();
let repo = FernetKeyRepository::new(dir.path().to_path_buf());
let null_key = URL_SAFE.encode([0u8; 32]);
std::fs::write(dir.path().join("0"), null_key).unwrap();

assert!(matches!(
repo.check_startup_null_key(false),
Err(CredentialFernetError::NullKeyDetected)
));
// Explicit opt-in allows startup to proceed.
assert!(repo.check_startup_null_key(true).is_ok());
}

#[test]
fn test_check_startup_null_key_passes_with_real_key() {
let dir = tempfile::tempdir().unwrap();
let repo = FernetKeyRepository::new(dir.path().to_path_buf());
repo.setup().unwrap();
assert!(repo.check_startup_null_key(false).is_ok());
}

#[test]
fn test_rotate_promotes_staged_key_and_keeps_old_primary_decryptable() {
let dir = tempfile::tempdir().unwrap();
Expand Down
11 changes: 11 additions & 0 deletions crates/keystone/src/bin/keystone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ use openstack_keystone_core::auth::ExecutionContext;
use openstack_keystone_core::cadf_hook::CadfAuditHook;
use openstack_keystone_core::db::sync_schema;
use openstack_keystone_core::error::KeystoneError;
use openstack_keystone_credential_driver_sql::fernet::FernetKeyRepository;
use openstack_keystone_distributed_storage::{StorageApi, app::Storage};

// Default body limit 256kB
Expand Down Expand Up @@ -174,6 +175,16 @@ async fn main() -> Result<(), Report> {

info!("Starting Keystone...");

// ADR 0019 §4: refuse to start if the credential Fernet key repository
// contains the well-known Null Key, unless the operator has explicitly
// opted in via [credential] insecure_allow_null_key. This is a startup
// check in addition to the check `FernetKeyRepository::load` already
// performs lazily on first credential access, so a misconfigured
// repository is caught immediately rather than on first request.
FernetKeyRepository::new(cfg.credential.key_repository.clone())
.check_startup_null_key(cfg.credential.insecure_allow_null_key)
.wrap_err("credential key repository failed startup check")?;

let token = CancellationToken::new();
let cloned_token = token.clone();

Expand Down
Loading