From 8368017fb2c9d5027ed518f3740b8572d6ef339f Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Fri, 31 Jul 2026 00:48:24 +0800 Subject: [PATCH] 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. --- crates/kms/src/backends/mod.rs | 1 + crates/kms/src/backends/vault.rs | 75 ++--- crates/kms/src/backends/vault_credentials.rs | 304 +++++++++++++++++++ crates/kms/src/backends/vault_transit.rs | 86 +++--- 4 files changed, 373 insertions(+), 93 deletions(-) create mode 100644 crates/kms/src/backends/vault_credentials.rs diff --git a/crates/kms/src/backends/mod.rs b/crates/kms/src/backends/mod.rs index 158bb4cc0..76da244f0 100644 --- a/crates/kms/src/backends/mod.rs +++ b/crates/kms/src/backends/mod.rs @@ -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 diff --git a/crates/kms/src/backends/vault.rs b/crates/kms/src/backends/vault.rs index f24b44427..109b9b12d 100644 --- a/crates/kms/src/backends/vault.rs +++ b/crates/kms/src/backends/vault.rs @@ -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 { - // 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 { + 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 { 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> { // 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), diff --git a/crates/kms/src/backends/vault_credentials.rs b/crates/kms/src/backends/vault_credentials.rs new file mode 100644 index 000000000..486100dcd --- /dev/null +++ b/crates/kms/src/backends/vault_credentials.rs @@ -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; +} + +/// 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 { + 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> { + 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, + /// 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 { + 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, +} + +impl VaultCredentialProvider { + /// Authenticate with `source` and build the initial client generation. + pub(crate) async fn new(settings: VaultConnectionSettings, source: Box) -> Result { + 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 { + 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)); + } +} diff --git a/crates/kms/src/backends/vault_transit.rs b/crates/kms/src/backends/vault_transit.rs index 72a895c2d..8ba3535a9 100644 --- a/crates/kms/src/backends/vault_transit.rs +++ b/crates/kms/src/backends/vault_transit.rs @@ -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 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 { - 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 { + self.credentials.current() + } + fn canonicalize_context(encryption_context: &HashMap) -> Result> { if encryption_context.is_empty() { return Ok(None); @@ -205,7 +192,7 @@ impl VaultTransitKmsClient { } async fn read_transit_key(&self, key_id: &str) -> Result { - 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> { let path = self.metadata_key_path(key_id); - match kv2::read::(&self.client, &self.metadata_kv_mount, &path).await { + match kv2::read::(&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 { - 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 { - 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?;