Watch
0
0
Fork
You've already forked hyperhive
0

swarm-secret-client: bound vault requests with a 30s timeout

vaultrs sets no request timeout by default, so a store that accepts a connection and never answers hung every SecretStore call forever. connect_with_identity now sets VAULT_REQUEST_TIMEOUT; a stalled store surfaces as Error::Vault.
This commit is contained in:
atlas 2026-10-09 14:38:04 +02:00 • committed by mara
commit 7c16212fdd
2 changed files with 36 additions and 13 deletions

View file

@ -1,10 +1,12 @@
//! A logged-in handle on the store, built from this deployment's environment.
use std::time::Duration;
use rustify_derive::Endpoint;
use serde::{Serialize, de::DeserializeOwned};
use vaultrs::{
api::pki::{requests::GenerateCertificateRequest, responses::GenerateCertificateResponse},
client::{Client, VaultClient, VaultClientSettingsBuilder},
client::{Client, VaultClient, VaultClientSettings, VaultClientSettingsBuilder},
};
use crate::{Error, mtls, path::MOUNT};
@ -23,6 +25,13 @@ pub const ENV_CACERT: &str = "BAO_CACERT";
/// The auth mount a cert login goes through, unless a caller says otherwise.
pub const DEFAULT_CERT_MOUNT: &str = "cert";
/// Upper bound on one request to the store, login included.
///
/// vaultrs sets no timeout by default, so a store that accepts the connection
/// and never answers would hold every [`SecretStore`] call open forever; with
/// this bound the call fails with [`Error::Vault`] instead.
const VAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
/// Where the store is and which identity we present to it.
///
/// Separate from the connect so the environment can be checked without one:
@ -37,6 +46,22 @@ pub struct Settings {
}
impl Settings {
/// The vaultrs settings for this store, presenting `identity`.
fn client_settings(
&self,
identity: Option<reqwest::Identity>,
) -> Result<VaultClientSettings, Error> {
let mut builder = VaultClientSettingsBuilder::default();
builder
.address(self.address.clone())
.identity(identity)
.timeout(Some(VAULT_REQUEST_TIMEOUT));
if let Some(ca) = &self.ca_path {
builder.ca_certs(vec![ca.clone()]);
}
builder.build().map_err(|e| Error::Settings(e.to_string()))
}
/// Read the `BAO_*` variables from the process environment.
///
/// # Errors
@ -147,16 +172,7 @@ impl SecretStore {
) -> Result<Self, Error> {
let identity = reqwest::Identity::from_pem(identity_pem).map_err(Error::Tls)?;
let mut builder = VaultClientSettingsBuilder::default();
builder
.address(settings.address.clone())
.identity(Some(identity));
if let Some(ca) = &settings.ca_path {
builder.ca_certs(vec![ca.clone()]);
}
let built = builder
.build()
.map_err(|e| Error::Settings(e.to_string()))?;
let built = settings.client_settings(Some(identity))?;
let mut inner = VaultClient::new(built)?;
let auth = vaultrs::auth::cert::login(&inner, cert_mount, cert_role).await?;
@ -471,6 +487,13 @@ mod tests {
}
}
#[test]
fn the_client_is_built_with_a_request_timeout() {
let s = Settings::from_lookup(full).expect("every required variable is set");
let built = s.client_settings(None).expect("settings build");
assert_eq!(built.timeout, Some(VAULT_REQUEST_TIMEOUT));
}
#[test]
fn a_complete_environment_is_accepted() {
// The control: without this, every assertion below could be passing

View file

@ -74,8 +74,8 @@ pub enum Error {
#[error("the store's settings are unusable: {0}")]
Settings(String),
/// The store refused us, was unreachable, or answered something we could
/// not parse.
/// The store refused us, was unreachable, did not answer within the
/// client's request timeout, or answered something we could not parse.
#[error(transparent)]
Vault(#[from] Box<vaultrs::error::ClientError>),