mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-31 10:32:24 +00:00
refactor(kms): route Vault clients through a rotatable credential provider (#5481)
* fix(kms): repair vault test call sites missed by the timeout refactor Three offline tests still constructed VaultKmsClient with the pre-#5472 single-argument signature, leaving cargo test -p rustfs-kms unable to compile. Pass the same 30s attempt timeout the neighbouring tests use. * refactor(kms): route Vault clients through a rotatable credential provider Both Vault backends previously built a VaultClient in their constructor and held it for the lifetime of the backend, which leaves no seam for re-authentication: rotating credentials would require tearing down the whole backend. Introduce backends/vault_credentials with a TokenSource trait (only StaticToken for now; AppRole login and agent token files land in follow-ups) and a VaultCredentialProvider that owns the authenticated client behind an ArcSwap. Request paths take a per-call snapshot via current(), so a future rotation swaps in a new client generation without interrupting calls already in flight. Tokens held by this crate are zeroized on drop, and Debug output of every credential-carrying type is redacted (covered by a leak regression test). Behavior is unchanged: static token, namespace, and per-attempt timeout feed the same VaultClientSettings as before, and AppRole configurations are still rejected at construction with the same message.
This commit is contained in:
@@ -22,6 +22,7 @@ use std::collections::HashMap;
|
||||
pub mod local;
|
||||
pub mod static_kms;
|
||||
pub mod vault;
|
||||
pub(crate) mod vault_credentials;
|
||||
pub mod vault_transit;
|
||||
|
||||
/// Abstract KMS client interface that all backends must implement
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
//! Vault-based KMS backend implementation using vaultrs
|
||||
|
||||
use crate::backends::vault_credentials::{VaultClientHandle, VaultConnectionSettings, VaultCredentialProvider, token_source_for};
|
||||
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
|
||||
use crate::config::{KmsConfig, VaultConfig};
|
||||
use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
||||
@@ -24,16 +25,14 @@ use base64::{Engine as _, engine::general_purpose};
|
||||
use jiff::Zoned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
use vaultrs::{
|
||||
client::{VaultClient, VaultClientSettingsBuilder},
|
||||
kv2,
|
||||
};
|
||||
use vaultrs::kv2;
|
||||
|
||||
/// Vault KMS client implementation
|
||||
pub struct VaultKmsClient {
|
||||
client: VaultClient,
|
||||
credentials: VaultCredentialProvider,
|
||||
config: VaultConfig,
|
||||
/// Mount path for the KV engine (typically "kv" or "secret")
|
||||
kv_mount: String,
|
||||
@@ -100,44 +99,18 @@ impl VaultKmsClient {
|
||||
///
|
||||
/// `attempt_timeout` caps every HTTP request issued through this client.
|
||||
pub async fn new(config: VaultConfig, attempt_timeout: Duration) -> Result<Self> {
|
||||
// Create client settings
|
||||
let mut settings_builder = VaultClientSettingsBuilder::default();
|
||||
settings_builder.address(&config.address);
|
||||
// Defense in depth against stalled connections: vaultrs leaves the
|
||||
// underlying reqwest client without any timeout by default, so a hung
|
||||
// request would otherwise wait forever regardless of the
|
||||
// operation-level retry policy.
|
||||
settings_builder.timeout(Some(attempt_timeout));
|
||||
|
||||
// Set authentication token based on method
|
||||
let token = match &config.auth_method {
|
||||
crate::config::VaultAuthMethod::Token { token } => token.clone(),
|
||||
crate::config::VaultAuthMethod::AppRole { .. } => {
|
||||
// For AppRole authentication, we would need to first authenticate
|
||||
// and get a token. For simplicity, we'll require a token for now.
|
||||
return Err(KmsError::backend_error(
|
||||
"AppRole authentication not yet implemented. Please use token authentication.",
|
||||
));
|
||||
}
|
||||
let source = token_source_for(&config.auth_method)?;
|
||||
let settings = VaultConnectionSettings {
|
||||
address: config.address.clone(),
|
||||
namespace: config.namespace.clone(),
|
||||
attempt_timeout,
|
||||
};
|
||||
|
||||
settings_builder.token(&token);
|
||||
|
||||
if let Some(namespace) = &config.namespace {
|
||||
settings_builder.namespace(Some(namespace.clone()));
|
||||
}
|
||||
|
||||
let settings = settings_builder
|
||||
.build()
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to build Vault client settings: {e}")))?;
|
||||
|
||||
let client =
|
||||
VaultClient::new(settings).map_err(|e| KmsError::backend_error(format!("Failed to create Vault client: {e}")))?;
|
||||
let credentials = VaultCredentialProvider::new(settings, source).await?;
|
||||
|
||||
info!(address = %config.address, "Vault KMS backend connected");
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
credentials,
|
||||
kv_mount: config.kv_mount.clone(),
|
||||
key_path_prefix: config.key_path_prefix.clone(),
|
||||
config,
|
||||
@@ -145,6 +118,14 @@ impl VaultKmsClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Snapshot the authenticated Vault client for a single request.
|
||||
///
|
||||
/// Every Vault call takes its own snapshot so a credential rotation
|
||||
/// applies to subsequent calls without interrupting in-flight ones.
|
||||
fn vault(&self) -> Arc<VaultClientHandle> {
|
||||
self.credentials.current()
|
||||
}
|
||||
|
||||
/// Get the full path for a key in Vault
|
||||
fn key_path(&self, key_id: &str) -> String {
|
||||
format!("{}/{}", self.key_path_prefix, key_id)
|
||||
@@ -193,7 +174,7 @@ impl VaultKmsClient {
|
||||
async fn store_key_data(&self, key_id: &str, key_data: &VaultKeyData) -> Result<()> {
|
||||
let path = self.key_path(key_id);
|
||||
|
||||
kv2::set(&self.client, &self.kv_mount, &path, key_data)
|
||||
kv2::set(&self.vault().client, &self.kv_mount, &path, key_data)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to store key in Vault: {e}")))?;
|
||||
|
||||
@@ -242,11 +223,13 @@ impl VaultKmsClient {
|
||||
async fn get_key_data(&self, key_id: &str) -> Result<VaultKeyData> {
|
||||
let path = self.key_path(key_id);
|
||||
|
||||
let secret: VaultKeyData = kv2::read(&self.client, &self.kv_mount, &path).await.map_err(|e| match e {
|
||||
vaultrs::error::ClientError::ResponseWrapError => KmsError::key_not_found(key_id),
|
||||
vaultrs::error::ClientError::APIError { code: 404, .. } => KmsError::key_not_found(key_id),
|
||||
_ => KmsError::backend_error(format!("Failed to read key from Vault: {e}")),
|
||||
})?;
|
||||
let secret: VaultKeyData = kv2::read(&self.vault().client, &self.kv_mount, &path)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
vaultrs::error::ClientError::ResponseWrapError => KmsError::key_not_found(key_id),
|
||||
vaultrs::error::ClientError::APIError { code: 404, .. } => KmsError::key_not_found(key_id),
|
||||
_ => KmsError::backend_error(format!("Failed to read key from Vault: {e}")),
|
||||
})?;
|
||||
|
||||
debug!("Retrieved key {} from Vault, tags: {:?}", key_id, secret.tags);
|
||||
Ok(secret)
|
||||
@@ -255,7 +238,7 @@ impl VaultKmsClient {
|
||||
/// List all keys stored in Vault
|
||||
async fn list_vault_keys(&self) -> Result<Vec<String>> {
|
||||
// List keys under the prefix
|
||||
match kv2::list(&self.client, &self.kv_mount, &self.key_path_prefix).await {
|
||||
match kv2::list(&self.vault().client, &self.kv_mount, &self.key_path_prefix).await {
|
||||
Ok(keys) => {
|
||||
debug!("Found {} keys in Vault", keys.len());
|
||||
Ok(keys)
|
||||
@@ -279,7 +262,7 @@ impl VaultKmsClient {
|
||||
|
||||
// For this specific key path, we can safely delete the metadata
|
||||
// since each key has its own unique path under the prefix
|
||||
kv2::delete_metadata(&self.client, &self.kv_mount, &path)
|
||||
kv2::delete_metadata(&self.vault().client, &self.kv_mount, &path)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
vaultrs::error::ClientError::APIError { code: 404, .. } => KmsError::key_not_found(key_id),
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Credential plumbing shared by the Vault KV2 and Transit backends.
|
||||
//!
|
||||
//! [`VaultCredentialProvider`] owns the authenticated [`VaultClient`] and hands
|
||||
//! out per-request snapshots. Backends take a fresh snapshot via
|
||||
//! [`VaultCredentialProvider::current`] for every Vault call instead of holding
|
||||
//! a client for their own lifetime: a future credential rotation then applies
|
||||
//! to the next call, while calls already in flight finish on the generation
|
||||
//! they captured (their `Arc` keeps it alive).
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::config::{VaultAuthMethod, redacted_secret};
|
||||
use crate::error::{KmsError, Result};
|
||||
|
||||
/// A Vault token handed out by a [`TokenSource`].
|
||||
///
|
||||
/// The crate's copy of the token is zeroized on drop. This cannot cover the
|
||||
/// copy `vaultrs` keeps inside its client settings (or the HTTP headers built
|
||||
/// from it); it bounds how long the token lingers in memory owned by this
|
||||
/// module.
|
||||
///
|
||||
/// AppRole login (PR-2) will extend this with the lease metadata returned by
|
||||
/// the login endpoint (`lease_duration`, `renewable`, accessor).
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub(crate) struct TokenLease {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl TokenLease {
|
||||
pub(crate) fn new(token: String) -> Self {
|
||||
Self { token }
|
||||
}
|
||||
|
||||
/// Expose the raw token for handing to the Vault client builder.
|
||||
pub(crate) fn expose(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TokenLease {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TokenLease")
|
||||
.field("token", &redacted_secret(&self.token))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Source of Vault authentication tokens.
|
||||
///
|
||||
/// Only [`StaticToken`] exists today. The trait is async and fallible so
|
||||
/// future sources can perform I/O when acquiring a token without changing the
|
||||
/// provider:
|
||||
/// - `AppRoleLogin` (PR-2): performs an `auth/approle/login` round trip and
|
||||
/// returns the issued token with its lease metadata;
|
||||
/// - `TokenFile` (PR-3): re-reads an agent-managed token file.
|
||||
#[async_trait]
|
||||
pub(crate) trait TokenSource: fmt::Debug + Send + Sync {
|
||||
/// Acquire a token for a new client generation.
|
||||
///
|
||||
/// Called once at provider construction today; rotation (PR-2) will call
|
||||
/// it again for every re-authentication.
|
||||
async fn acquire(&self) -> Result<TokenLease>;
|
||||
}
|
||||
|
||||
/// Token source for [`VaultAuthMethod::Token`]: always yields the token fixed
|
||||
/// at configuration time.
|
||||
pub(crate) struct StaticToken {
|
||||
token: TokenLease,
|
||||
}
|
||||
|
||||
impl StaticToken {
|
||||
pub(crate) fn new(token: String) -> Self {
|
||||
Self {
|
||||
token: TokenLease::new(token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TokenSource for StaticToken {
|
||||
async fn acquire(&self) -> Result<TokenLease> {
|
||||
Ok(self.token.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StaticToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// TokenLease::fmt already redacts the token value.
|
||||
f.debug_struct("StaticToken").field("token", &self.token).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the configured auth method onto a token source.
|
||||
///
|
||||
/// AppRole is still rejected at construction time; PR-2 replaces this arm with
|
||||
/// an `AppRoleLogin` source.
|
||||
pub(crate) fn token_source_for(auth_method: &VaultAuthMethod) -> Result<Box<dyn TokenSource>> {
|
||||
match auth_method {
|
||||
VaultAuthMethod::Token { token } => Ok(Box::new(StaticToken::new(token.clone()))),
|
||||
VaultAuthMethod::AppRole { .. } => Err(KmsError::backend_error(
|
||||
"AppRole authentication not yet implemented. Please use token authentication.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection parameters shared by every client generation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct VaultConnectionSettings {
|
||||
pub(crate) address: String,
|
||||
pub(crate) namespace: Option<String>,
|
||||
/// Per-attempt HTTP timeout applied to the underlying reqwest client.
|
||||
pub(crate) attempt_timeout: Duration,
|
||||
}
|
||||
|
||||
impl VaultConnectionSettings {
|
||||
/// Build an authenticated client for one generation.
|
||||
fn build_client(&self, token: &TokenLease) -> Result<VaultClient> {
|
||||
let mut settings_builder = VaultClientSettingsBuilder::default();
|
||||
settings_builder.address(&self.address);
|
||||
// Defense in depth against stalled connections: vaultrs leaves the
|
||||
// underlying reqwest client without any timeout by default, so a hung
|
||||
// request would otherwise wait forever regardless of the
|
||||
// operation-level retry policy.
|
||||
settings_builder.timeout(Some(self.attempt_timeout));
|
||||
settings_builder.token(token.expose());
|
||||
|
||||
if let Some(namespace) = &self.namespace {
|
||||
settings_builder.namespace(Some(namespace.clone()));
|
||||
}
|
||||
|
||||
let settings = settings_builder
|
||||
.build()
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to build Vault client settings: {e}")))?;
|
||||
|
||||
VaultClient::new(settings).map_err(|e| KmsError::backend_error(format!("Failed to create Vault client: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// One authenticated client generation.
|
||||
///
|
||||
/// Request paths hold the handle (via `Arc`) for the duration of a single
|
||||
/// Vault call, so a rotation that swaps in a newer generation never tears the
|
||||
/// client out from under an in-flight request.
|
||||
pub(crate) struct VaultClientHandle {
|
||||
/// Monotonic counter identifying the credential generation this client was
|
||||
/// built from. Static tokens never rotate, so only generation 0 exists
|
||||
/// today; rotation (PR-2) bumps it on every re-authentication.
|
||||
pub(crate) generation: u64,
|
||||
pub(crate) client: VaultClient,
|
||||
}
|
||||
|
||||
impl fmt::Debug for VaultClientHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// `VaultClient` embeds its settings, including the token, so it must
|
||||
// never appear in Debug output.
|
||||
f.debug_struct("VaultClientHandle")
|
||||
.field("generation", &self.generation)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the authenticated Vault client for a backend and hands out
|
||||
/// per-request snapshots.
|
||||
///
|
||||
/// The provider keeps neither the settings nor the source after construction
|
||||
/// because a static token can never be refreshed. Rotation (PR-2) will retain
|
||||
/// both and add a refresh path that acquires a fresh lease, rebuilds the
|
||||
/// client, and stores it under a bumped generation.
|
||||
pub(crate) struct VaultCredentialProvider {
|
||||
current: ArcSwap<VaultClientHandle>,
|
||||
}
|
||||
|
||||
impl VaultCredentialProvider {
|
||||
/// Authenticate with `source` and build the initial client generation.
|
||||
pub(crate) async fn new(settings: VaultConnectionSettings, source: Box<dyn TokenSource>) -> Result<Self> {
|
||||
let lease = source.acquire().await?;
|
||||
let client = settings.build_client(&lease)?;
|
||||
Ok(Self {
|
||||
current: ArcSwap::from_pointee(VaultClientHandle { generation: 0, client }),
|
||||
})
|
||||
}
|
||||
|
||||
/// Snapshot the current client generation.
|
||||
///
|
||||
/// Take one snapshot per Vault call: the returned `Arc` pins the
|
||||
/// generation for exactly that call, so a concurrent rotation applies to
|
||||
/// the next call without interrupting this one.
|
||||
pub(crate) fn current(&self) -> Arc<VaultClientHandle> {
|
||||
self.current.load_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VaultCredentialProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("VaultCredentialProvider")
|
||||
.field("current", &self.current.load())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::REDACTED_SECRET;
|
||||
|
||||
const TEST_TOKEN: &str = "vault-token-debug-leak-canary";
|
||||
|
||||
fn test_settings() -> VaultConnectionSettings {
|
||||
VaultConnectionSettings {
|
||||
address: "http://127.0.0.1:8200".to_string(),
|
||||
namespace: Some("team-namespace".to_string()),
|
||||
attempt_timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
|
||||
/// Building a provider never contacts Vault, so these tests run offline.
|
||||
async fn test_provider() -> VaultCredentialProvider {
|
||||
VaultCredentialProvider::new(test_settings(), Box::new(StaticToken::new(TEST_TOKEN.to_string())))
|
||||
.await
|
||||
.expect("provider construction must not require a live Vault")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_static_token_snapshots_pin_one_generation() {
|
||||
let provider = test_provider().await;
|
||||
|
||||
let first = provider.current();
|
||||
let second = provider.current();
|
||||
|
||||
assert_eq!(first.generation, 0);
|
||||
assert!(
|
||||
Arc::ptr_eq(&first, &second),
|
||||
"without rotation every snapshot must return the same client generation"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_static_token_source_yields_configured_token() {
|
||||
let source = token_source_for(&VaultAuthMethod::Token {
|
||||
token: TEST_TOKEN.to_string(),
|
||||
})
|
||||
.expect("token auth must map to a source");
|
||||
|
||||
let lease = source.acquire().await.expect("static acquire cannot fail");
|
||||
assert_eq!(lease.expose(), TEST_TOKEN);
|
||||
}
|
||||
|
||||
/// Behavior pin: AppRole keeps failing at construction with the same
|
||||
/// user-visible message until the login source lands (PR-2).
|
||||
#[test]
|
||||
fn test_approle_auth_method_still_rejected() {
|
||||
let error = token_source_for(&VaultAuthMethod::AppRole {
|
||||
role_id: "role".to_string(),
|
||||
secret_id: "approle-secret-canary".to_string(),
|
||||
})
|
||||
.expect_err("approle must stay rejected until the login source lands");
|
||||
|
||||
let rendered = error.to_string();
|
||||
assert!(rendered.contains("AppRole authentication not yet implemented"), "got: {rendered}");
|
||||
assert!(!rendered.contains("approle-secret-canary"), "error must not echo the secret id");
|
||||
}
|
||||
|
||||
/// Leak regression: the Debug output of every credential-carrying type
|
||||
/// must stay free of the token literal.
|
||||
#[tokio::test]
|
||||
async fn test_credential_types_debug_redacts_token() {
|
||||
let provider = test_provider().await;
|
||||
let handle = provider.current();
|
||||
let lease = TokenLease::new(TEST_TOKEN.to_string());
|
||||
let source = StaticToken::new(TEST_TOKEN.to_string());
|
||||
|
||||
for rendered in [
|
||||
format!("{provider:?}"),
|
||||
format!("{handle:?}"),
|
||||
format!("{lease:?}"),
|
||||
format!("{source:?}"),
|
||||
] {
|
||||
assert!(!rendered.contains(TEST_TOKEN), "debug output must not leak the vault token: {rendered}");
|
||||
}
|
||||
|
||||
assert!(format!("{lease:?}").contains(REDACTED_SECRET));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
//! Vault Transit-based KMS backend.
|
||||
|
||||
use crate::backends::vault_credentials::{VaultClientHandle, VaultConnectionSettings, VaultCredentialProvider, token_source_for};
|
||||
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
|
||||
use crate::config::{KmsConfig, VaultTransitConfig};
|
||||
use crate::encryption::{DataKeyEnvelope, generate_key_material};
|
||||
@@ -24,6 +25,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use jiff::Zoned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use vaultrs::{
|
||||
@@ -33,7 +35,6 @@ use vaultrs::{
|
||||
CreateKeyRequestBuilder, DecryptDataRequestBuilder, EncryptDataRequestBuilder, UpdateKeyConfigurationRequestBuilder,
|
||||
},
|
||||
},
|
||||
client::{VaultClient, VaultClientSettingsBuilder},
|
||||
kv2,
|
||||
transit::{data, key},
|
||||
};
|
||||
@@ -128,7 +129,7 @@ impl From<TransitKeyMetadataPersisted> for TransitKeyMetadata {
|
||||
}
|
||||
|
||||
pub struct VaultTransitKmsClient {
|
||||
client: VaultClient,
|
||||
credentials: VaultCredentialProvider,
|
||||
config: VaultTransitConfig,
|
||||
/// KV v2 mount path for persisting transit key metadata
|
||||
metadata_kv_mount: String,
|
||||
@@ -142,38 +143,16 @@ impl VaultTransitKmsClient {
|
||||
///
|
||||
/// `attempt_timeout` caps every HTTP request issued through this client.
|
||||
pub async fn new(config: VaultTransitConfig, attempt_timeout: Duration) -> Result<Self> {
|
||||
let mut settings_builder = VaultClientSettingsBuilder::default();
|
||||
settings_builder.address(&config.address);
|
||||
// Defense in depth against stalled connections: vaultrs leaves the
|
||||
// underlying reqwest client without any timeout by default, so a hung
|
||||
// request would otherwise wait forever regardless of the
|
||||
// operation-level retry policy.
|
||||
settings_builder.timeout(Some(attempt_timeout));
|
||||
|
||||
let token = match &config.auth_method {
|
||||
crate::config::VaultAuthMethod::Token { token } => token.clone(),
|
||||
crate::config::VaultAuthMethod::AppRole { .. } => {
|
||||
return Err(KmsError::backend_error(
|
||||
"AppRole authentication not yet implemented. Please use token authentication.",
|
||||
));
|
||||
}
|
||||
let source = token_source_for(&config.auth_method)?;
|
||||
let settings = VaultConnectionSettings {
|
||||
address: config.address.clone(),
|
||||
namespace: config.namespace.clone(),
|
||||
attempt_timeout,
|
||||
};
|
||||
|
||||
settings_builder.token(&token);
|
||||
|
||||
if let Some(namespace) = &config.namespace {
|
||||
settings_builder.namespace(Some(namespace.clone()));
|
||||
}
|
||||
|
||||
let settings = settings_builder
|
||||
.build()
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to build Vault client settings: {e}")))?;
|
||||
|
||||
let client =
|
||||
VaultClient::new(settings).map_err(|e| KmsError::backend_error(format!("Failed to create Vault client: {e}")))?;
|
||||
let credentials = VaultCredentialProvider::new(settings, source).await?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
credentials,
|
||||
metadata_kv_mount: config.metadata_kv_mount.clone(),
|
||||
metadata_key_prefix: config.metadata_key_prefix.clone(),
|
||||
config,
|
||||
@@ -181,6 +160,14 @@ impl VaultTransitKmsClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Snapshot the authenticated Vault client for a single request.
|
||||
///
|
||||
/// Every Vault call takes its own snapshot so a credential rotation
|
||||
/// applies to subsequent calls without interrupting in-flight ones.
|
||||
fn vault(&self) -> Arc<VaultClientHandle> {
|
||||
self.credentials.current()
|
||||
}
|
||||
|
||||
fn canonicalize_context(encryption_context: &HashMap<String, String>) -> Result<Option<String>> {
|
||||
if encryption_context.is_empty() {
|
||||
return Ok(None);
|
||||
@@ -205,7 +192,7 @@ impl VaultTransitKmsClient {
|
||||
}
|
||||
|
||||
async fn read_transit_key(&self, key_id: &str) -> Result<vaultrs::api::transit::responses::ReadKeyResponse> {
|
||||
key::read(&self.client, &self.config.mount_path, key_id)
|
||||
key::read(&self.vault().client, &self.config.mount_path, key_id)
|
||||
.await
|
||||
.or_else(|e| Self::map_vault_error(key_id, e, "read"))
|
||||
}
|
||||
@@ -213,7 +200,7 @@ impl VaultTransitKmsClient {
|
||||
async fn create_transit_key(&self, key_id: &str) -> Result<()> {
|
||||
let mut builder = CreateKeyRequestBuilder::default();
|
||||
builder.key_type(KeyType::Aes256Gcm96);
|
||||
key::create(&self.client, &self.config.mount_path, key_id, Some(&mut builder))
|
||||
key::create(&self.vault().client, &self.config.mount_path, key_id, Some(&mut builder))
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to create Vault Transit key {key_id}: {e}")))
|
||||
}
|
||||
@@ -230,7 +217,7 @@ impl VaultTransitKmsClient {
|
||||
builder.associated_data(aad);
|
||||
}
|
||||
|
||||
let response = data::encrypt(&self.client, &self.config.mount_path, key_id, &plaintext_b64, Some(&mut builder))
|
||||
let response = data::encrypt(&self.vault().client, &self.config.mount_path, key_id, &plaintext_b64, Some(&mut builder))
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to encrypt data with Vault Transit key {key_id}: {e}")))?;
|
||||
|
||||
@@ -248,7 +235,7 @@ impl VaultTransitKmsClient {
|
||||
builder.associated_data(aad);
|
||||
}
|
||||
|
||||
let response = data::decrypt(&self.client, &self.config.mount_path, key_id, ciphertext, Some(&mut builder))
|
||||
let response = data::decrypt(&self.vault().client, &self.config.mount_path, key_id, ciphertext, Some(&mut builder))
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to decrypt data with Vault Transit key {key_id}: {e}")))?;
|
||||
|
||||
@@ -263,7 +250,7 @@ impl VaultTransitKmsClient {
|
||||
|
||||
async fn read_metadata_from_kv(&self, key_id: &str) -> Result<Option<TransitKeyMetadata>> {
|
||||
let path = self.metadata_key_path(key_id);
|
||||
match kv2::read::<TransitKeyMetadataPersisted>(&self.client, &self.metadata_kv_mount, &path).await {
|
||||
match kv2::read::<TransitKeyMetadataPersisted>(&self.vault().client, &self.metadata_kv_mount, &path).await {
|
||||
Ok(persisted) => Ok(Some(persisted.into())),
|
||||
Err(vaultrs::error::ClientError::ResponseWrapError)
|
||||
| Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(None),
|
||||
@@ -274,7 +261,7 @@ impl VaultTransitKmsClient {
|
||||
async fn write_metadata_to_kv(&self, key_id: &str, metadata: &TransitKeyMetadata) -> Result<()> {
|
||||
let path = self.metadata_key_path(key_id);
|
||||
let persisted: TransitKeyMetadataPersisted = metadata.clone().into();
|
||||
kv2::set(&self.client, &self.metadata_kv_mount, &path, &persisted)
|
||||
kv2::set(&self.vault().client, &self.metadata_kv_mount, &path, &persisted)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to write transit key metadata to Vault KV: {e}")))
|
||||
@@ -282,7 +269,7 @@ impl VaultTransitKmsClient {
|
||||
|
||||
async fn delete_metadata_from_kv(&self, key_id: &str) -> Result<()> {
|
||||
let path = self.metadata_key_path(key_id);
|
||||
match kv2::delete_metadata(&self.client, &self.metadata_kv_mount, &path).await {
|
||||
match kv2::delete_metadata(&self.vault().client, &self.metadata_kv_mount, &path).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(vaultrs::error::ClientError::ResponseWrapError)
|
||||
| Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(()),
|
||||
@@ -496,7 +483,7 @@ impl KmsClient for VaultTransitKmsClient {
|
||||
}
|
||||
|
||||
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
|
||||
let all_keys = key::list(&self.client, &self.config.mount_path)
|
||||
let all_keys = key::list(&self.vault().client, &self.config.mount_path)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to list Vault Transit keys: {e}")))?
|
||||
.keys;
|
||||
@@ -566,7 +553,7 @@ impl KmsClient for VaultTransitKmsClient {
|
||||
}
|
||||
|
||||
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKeyInfo> {
|
||||
key::rotate(&self.client, &self.config.mount_path, key_id)
|
||||
key::rotate(&self.vault().client, &self.config.mount_path, key_id)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to rotate Vault Transit key {key_id}: {e}")))?;
|
||||
|
||||
@@ -589,7 +576,7 @@ impl KmsClient for VaultTransitKmsClient {
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> Result<()> {
|
||||
key::list(&self.client, &self.config.mount_path)
|
||||
key::list(&self.vault().client, &self.config.mount_path)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| KmsError::backend_error(format!("Vault Transit health check failed: {e}")))
|
||||
@@ -710,13 +697,18 @@ impl KmsBackend for VaultTransitKmsBackend {
|
||||
if !self.client.read_transit_key(&key_id).await?.deletion_allowed {
|
||||
let mut update_builder = UpdateKeyConfigurationRequestBuilder::default();
|
||||
update_builder.deletion_allowed(true);
|
||||
key::update(&self.client.client, &self.client.config.mount_path, &key_id, Some(&mut update_builder))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
KmsError::backend_error(format!("Failed to allow deletion of Vault Transit key {key_id}: {e}"))
|
||||
})?;
|
||||
key::update(
|
||||
&self.client.vault().client,
|
||||
&self.client.config.mount_path,
|
||||
&key_id,
|
||||
Some(&mut update_builder),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
KmsError::backend_error(format!("Failed to allow deletion of Vault Transit key {key_id}: {e}"))
|
||||
})?;
|
||||
}
|
||||
key::delete(&self.client.client, &self.client.config.mount_path, &key_id)
|
||||
key::delete(&self.client.vault().client, &self.client.config.mount_path, &key_id)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to delete Vault Transit key {key_id}: {e}")))?;
|
||||
self.client.delete_key_metadata(&key_id).await?;
|
||||
|
||||
Reference in New Issue
Block a user