Watch
0
0
Fork
You've already forked hyperhive
0

give cert-auth role tokens a 15m ttl so logins stop leaving 32-day leases

This commit is contained in:
atlas 2026-10-28 18:32:44 +01:00 • committed by mara
commit 00df3ccffb

View file

@ -5,7 +5,10 @@ use std::time::Duration;
use rustify_derive::Endpoint;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use vaultrs::{
api::pki::{requests::GenerateCertificateRequest, responses::GenerateCertificateResponse},
api::{
auth::cert::requests::CreateCaCertificateRoleRequest,
pki::{requests::GenerateCertificateRequest, responses::GenerateCertificateResponse},
},
client::{Client, VaultClient, VaultClientSettings, VaultClientSettingsBuilder},
};
@ -25,6 +28,15 @@ 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";
/// How long a token from a cert login lives, and the longest it can be made to.
///
/// Every cert-auth role is written with it as `token_ttl` and `token_max_ttl`.
/// The store keeps a lease for each token until it expires, so a role without
/// it leaves one lease per login for the mount's default of 32 days.
/// [`SecretStore`] holds the token it logs in with and renews nothing: log in
/// again rather than keep a handle past this.
pub const CERT_ROLE_TOKEN_TTL: &str = "15m";
/// Upper bound on one request to the store, login included.
///
/// vaultrs sets no timeout by default, so a store that accepts the connection
@ -371,18 +383,8 @@ impl SecretStore {
common_name: &str,
policies: &[&str],
) -> Result<(), Error> {
let mut opts =
vaultrs::api::auth::cert::requests::CreateCaCertificateRoleRequest::builder();
opts.allowed_common_names(vec![common_name.to_owned()])
.token_policies(policies.iter().map(|p| (*p).to_owned()).collect::<Vec<_>>());
vaultrs::auth::cert::ca_cert_role::set(
&self.inner,
mount,
name,
certificate,
Some(&mut opts),
)
.await?;
let request = cert_role_request(mount, name, certificate, common_name, policies);
vaultrs::api::exec_with_empty(&self.inner, request).await?;
Ok(())
}
@ -667,6 +669,28 @@ fn issue_request(mount: &str, role: &str, common_name: &str) -> GenerateCertific
}
}
/// The request that writes a cert-auth role: `certificate` pinned, only
/// `common_name` accepted, `policies` attached, tokens living
/// [`CERT_ROLE_TOKEN_TTL`].
fn cert_role_request(
mount: &str,
name: &str,
certificate: &str,
common_name: &str,
policies: &[&str],
) -> CreateCaCertificateRoleRequest {
CreateCaCertificateRoleRequest {
mount: mount.to_owned(),
name: name.to_owned(),
certificate: certificate.to_owned(),
allowed_common_names: Some(vec![common_name.to_owned()]),
token_policies: Some(policies.iter().map(|p| (*p).to_owned()).collect()),
token_ttl: Some(CERT_ROLE_TOKEN_TTL.to_owned()),
token_max_ttl: Some(CERT_ROLE_TOKEN_TTL.to_owned()),
..CreateCaCertificateRoleRequest::default()
}
}
/// The store's answer, moved straight into the redacting
/// [`mtls::Credential`]. `GenerateCertificateResponse` derives `Debug` and
/// holds the private key, so it is consumed here and never formatted.
@ -711,6 +735,20 @@ mod tests {
assert_eq!(built.timeout, Some(VAULT_REQUEST_TIMEOUT));
}
#[test]
fn a_cert_role_is_written_with_short_lived_tokens() {
let r = cert_role_request("cert", "hive-agent-a", "PEM", "hive-agent-a", &["shared"]);
assert_eq!(r.token_ttl.as_deref(), Some("15m"));
assert_eq!(r.token_max_ttl.as_deref(), Some("15m"));
// The control: the rest of the role is still written.
assert_eq!(
r.allowed_common_names,
Some(vec!["hive-agent-a".to_owned()])
);
assert_eq!(r.token_policies, Some(vec!["shared".to_owned()]));
assert_eq!(r.certificate, "PEM");
}
#[test]
fn a_complete_environment_is_accepted() {
// The control: without this, every assertion below could be passing