Skip to content

feat(config): Add Vault-backed configuration#1051

Open
magdang wants to merge 1 commit into
openstack-experimental:mainfrom
magdang:feat/502-vault-config
Open

feat(config): Add Vault-backed configuration#1051
magdang wants to merge 1 commit into
openstack-experimental:mainfrom
magdang:feat/502-vault-config

Conversation

@magdang

@magdang magdang commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Resolve complete vault://mount/path#key configuration values directly from Vault KV v2.

Changes

  • Add optional [vault] bootstrap configuration and async resolution after configuration source precedence is merged.
  • Poll KV v2 metadata, renew renewable tokens, and atomically retain the last-known-good configuration on failures.
  • Migrate Keystone and keystone-manage startup to the asynchronous loader.
  • Add mock Vault coverage for validation, deduplication, redaction, renewal, reloads, and last-known-good retention.

Tests

  • Config: 106 unit tests and 2 doctests passed.
  • Keystone manage: 95 tests passed.
  • Keystone: 659 tests passed.
  • Targeted Clippy, cargo fmt, pre-commit, and git diff check passed.

Closes #502

@magdang
magdang force-pushed the feat/502-vault-config branch from 4cbc673 to 4d3126b Compare July 17, 2026 14:12
@magdang

magdang commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

For the parts of #502 that were not fully specified, I took the following approach:

  • Support token authentication and KV v2 only.
  • Resolve complete vault:///# values after configuration source precedence is merged.
  • Keep [vault] as bootstrap configuration and exclude it from secret resolution.
  • Poll KV v2 metadata for new versions because KV v2 secrets do not provide renewable leases.
  • Renew renewable Vault tokens halfway through their TTL.
  • Rebuild and validate the complete configuration before applying an update.
  • Retain the last-known-good configuration if a runtime refresh fails.
  • Keep synchronous loaders for configurations without Vault references and require the async loader when references are present.

I kept AppRole, Kubernetes authentication, dynamic secret engines, and Fernet key-source integration out of scope.

@magdang
magdang requested a review from gtema July 17, 2026 14:20
@gtema
gtema force-pushed the feat/502-vault-config branch from 4d3126b to 7253cd5 Compare July 17, 2026 15:49

@gtema gtema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice, need small changes though

Comment thread crates/config/src/lib.rs Outdated
let deadline = vault_runtime
.as_ref()
.map(vault::VaultRuntime::next_deadline)
.unwrap_or_else(|| Instant::now() + std::time::Duration::from_secs(31_536_000));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 31_536_000s (1yr) magic sentinel for "no active vault runtime, never fire" deadline in tokio::select!. Prefer std::future::pending() for the no-vault branch — clearer intent, no magic number.

Something like this can be used, but feel free to use different approach

// 1. Define the future dynamically before the select block
let vault_deadline_fut = match &vault_runtime {
    Some(runtime) => {
        let deadline = runtime.next_deadline();
        futures::future::Either::Left(tokio::time::sleep_until(deadline))
    }
    None => futures::future::Either::Right(std::future::pending::<()>()),
};

tokio::select! {
    event = sync_rx.recv() => {
        // ... (your existing sync_rx logic)
    }
    // 2. Await the dynamic future directly
    _ = vault_deadline_fut => {
        // Safe to unwrap here because this branch is unreachable if vault_runtime was None
        let runtime = vault_runtime.as_mut().unwrap(); 
        
        if runtime.renew_if_due().await.is_err() {
            error!("Vault token renewal failed; retrying while retaining current configuration");
        }
        // ... (rest of your renewal / polling logic)
    }
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Dropped the sentinel. The select! now arms sleep_until(runtime.next_deadline()) only when a Vault runtime is present, and falls back to std::future::pending() otherwise:

let vault_deadline = vault_runtime.as_ref().map(vault::VaultRuntime::next_deadline);
let vault_tick = async {
    match vault_deadline {
        Some(deadline) => tokio::time::sleep_until(deadline).await,
        None => std::future::pending::<()>().await,
    }
};

Used a plain async block instead of future::Either to avoid pulling in futures.

Comment thread crates/config/src/lib.rs Outdated
let raw = Self::build_raw(path)?;
if vault::contains_vault_references(&raw.cache)? {
return Err(vault::VaultConfigError::AsyncLoaderRequired.into());
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not fan of such logic (raising error on sync call requiring async. This requires callers to know how to deal with the error with none of the user having any logic for this. Basically it renders the feature right now as useless. Let's make it async-only accepting wider interface change (mostly only adding .await for callers)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Made the loaders async-only. Config::new and Config::load_all are now async and resolve Vault references directly, so the AsyncLoaderRequired error path is gone from the file loaders. Removed the redundant load_all_async and folded it into load_all, with a shared resolve_vault_references helper. Callers just gained .await.

The only remaining sync guard is on TryFrom<ConfigBuilder>, an in-memory test-only conversion that can't be made async. Happy to drop it too if you prefer.

@magdang
magdang force-pushed the feat/502-vault-config branch 2 times, most recently from c2262e4 to 6c59bab Compare July 18, 2026 18:41
@magdang
magdang requested a review from gtema July 18, 2026 18:49
Resolve complete Vault references from KV v2 after source merging.

Poll metadata for rotations and retain the last complete configuration.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Yousef Hussein <[email protected]>
@magdang
magdang force-pushed the feat/502-vault-config branch from 6c59bab to 92508bb Compare July 19, 2026 23:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend config manager with vault support

2 participants