bring existing agent cert roles to the 1h token ttl on controller start
This commit is contained in:
parent
e8190a4972
commit
76de27fc97
1 changed files with 513 additions and 13 deletions
|
|
@ -37,6 +37,10 @@ pub const DEFAULT_CERT_MOUNT: &str = "cert";
|
|||
/// again rather than keep a handle past this.
|
||||
pub const CERT_ROLE_TOKEN_TTL: &str = "1h";
|
||||
|
||||
/// [`CERT_ROLE_TOKEN_TTL`] in seconds, the unit the store reports a role's
|
||||
/// lifetimes in.
|
||||
pub const CERT_ROLE_TOKEN_TTL_SECS: u64 = 3600;
|
||||
|
||||
/// Upper bound on one request to the store, login included.
|
||||
///
|
||||
/// vaultrs sets no timeout by default, so a store that accepts the connection
|
||||
|
|
@ -388,6 +392,46 @@ impl SecretStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The token lifetimes of the cert-auth role `name`, or `None` when the
|
||||
/// role does not exist.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Vault`] for anything that is not a 404: a denial, or an
|
||||
/// unreachable store.
|
||||
pub async fn cert_role_token_ttls(
|
||||
&self,
|
||||
mount: &str,
|
||||
name: &str,
|
||||
) -> Result<Option<CertRoleTokenTtls>, Error> {
|
||||
let request = ReadCertRoleTtls {
|
||||
mount: mount.to_owned(),
|
||||
name: name.to_owned(),
|
||||
};
|
||||
match vaultrs::api::exec_with_result(&self.inner, request).await {
|
||||
Ok(ttls) => Ok(Some(ttls)),
|
||||
Err(e) if is_absent(&e) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the token lifetimes of the existing cert-auth role `name` to
|
||||
/// [`CERT_ROLE_TOKEN_TTL`], leaving the rest of the role as it is.
|
||||
///
|
||||
/// The write names no other field, and the store keeps the fields a write
|
||||
/// leaves out (`builtin/credential/cert/path_certs.go`, `pathCertWrite`,
|
||||
/// `OpenBao` 2.6.5), so the role's certificate, allowed names and policies
|
||||
/// stay. A role that does not exist would be created without a certificate
|
||||
/// and refused; callers read the role first.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Vault`] when the token's policy does not cover
|
||||
/// `auth/<mount>/certs/<name>`, or the store refuses the write.
|
||||
pub async fn set_cert_role_token_ttls(&self, mount: &str, name: &str) -> Result<(), Error> {
|
||||
vaultrs::api::exec_with_empty(&self.inner, set_cert_role_token_ttls_request(mount, name))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Have the PKI role `role` on `mount` issue a client certificate for
|
||||
/// `common_name`, returning it with its key and the issuing CA.
|
||||
///
|
||||
|
|
@ -647,6 +691,61 @@ struct WriteAclPolicy {
|
|||
policy: String,
|
||||
}
|
||||
|
||||
/// A cert-auth role's token lifetimes in seconds, `0` when unset (the mount's
|
||||
/// default then applies). The role's certificate and names are not read.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
pub struct CertRoleTokenTtls {
|
||||
pub token_ttl: u64,
|
||||
pub token_max_ttl: u64,
|
||||
}
|
||||
|
||||
impl CertRoleTokenTtls {
|
||||
/// Whether both lifetimes are [`CERT_ROLE_TOKEN_TTL`].
|
||||
#[must_use]
|
||||
pub fn is_current(self) -> bool {
|
||||
self.token_ttl == CERT_ROLE_TOKEN_TTL_SECS && self.token_max_ttl == CERT_ROLE_TOKEN_TTL_SECS
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a cert-auth role. Spelled out because vaultrs's response type requires
|
||||
/// every field the store answers with, and only the two lifetimes are wanted.
|
||||
#[derive(Endpoint)]
|
||||
#[endpoint(
|
||||
path = "auth/{self.mount}/certs/{self.name}",
|
||||
method = "GET",
|
||||
response = "CertRoleTokenTtls"
|
||||
)]
|
||||
struct ReadCertRoleTtls {
|
||||
#[endpoint(skip)]
|
||||
mount: String,
|
||||
#[endpoint(skip)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Write the two token lifetimes of a cert-auth role and nothing else.
|
||||
/// vaultrs's `CreateCaCertificateRoleRequest` always sends `certificate`, which
|
||||
/// would replace the role's.
|
||||
#[derive(Endpoint)]
|
||||
#[endpoint(path = "auth/{self.mount}/certs/{self.name}", method = "POST")]
|
||||
struct SetCertRoleTtls {
|
||||
#[endpoint(skip)]
|
||||
mount: String,
|
||||
#[endpoint(skip)]
|
||||
name: String,
|
||||
token_ttl: String,
|
||||
token_max_ttl: String,
|
||||
}
|
||||
|
||||
/// The request [`SecretStore::set_cert_role_token_ttls`] sends.
|
||||
fn set_cert_role_token_ttls_request(mount: &str, name: &str) -> SetCertRoleTtls {
|
||||
SetCertRoleTtls {
|
||||
mount: mount.to_owned(),
|
||||
name: name.to_owned(),
|
||||
token_ttl: CERT_ROLE_TOKEN_TTL.to_owned(),
|
||||
token_max_ttl: CERT_ROLE_TOKEN_TTL.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The options [`SecretStore::create`] writes with: version 0, which only a
|
||||
/// path holding nothing is at.
|
||||
fn create_only() -> vaultrs::api::kv2::requests::SetSecretRequestOptions {
|
||||
|
|
@ -749,6 +848,48 @@ mod tests {
|
|||
assert_eq!(r.certificate, "PEM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ttl_in_seconds_is_the_ttl_the_roles_are_written_with() {
|
||||
assert_eq!(CERT_ROLE_TOKEN_TTL, "1h");
|
||||
assert_eq!(CERT_ROLE_TOKEN_TTL_SECS, 3600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_role_is_current_only_when_both_lifetimes_are_the_ttl() {
|
||||
let ttls = |token_ttl, token_max_ttl| CertRoleTokenTtls {
|
||||
token_ttl,
|
||||
token_max_ttl,
|
||||
};
|
||||
assert!(ttls(3600, 3600).is_current());
|
||||
assert!(!ttls(0, 0).is_current(), "unset takes the mount default");
|
||||
assert!(!ttls(3600, 0).is_current());
|
||||
assert!(!ttls(0, 3600).is_current());
|
||||
assert!(!ttls(900, 900).is_current());
|
||||
}
|
||||
|
||||
/// The store keeps the fields a write leaves out, so the write names these
|
||||
/// two and no others: a `certificate` here would replace the role's.
|
||||
#[test]
|
||||
fn the_ttl_write_names_the_two_lifetimes_and_nothing_else() {
|
||||
use rustify::Endpoint as _;
|
||||
let r = set_cert_role_token_ttls_request("cert", "hive-agent-a")
|
||||
.request("http://store.invalid")
|
||||
.expect("builds");
|
||||
assert_eq!(r.uri().path(), "/auth/cert/certs/hive-agent-a");
|
||||
let body: serde_json::Value = serde_json::from_slice(r.body()).expect("json body");
|
||||
assert_eq!(
|
||||
body,
|
||||
serde_json::json!({"token_ttl": "1h", "token_max_ttl": "1h"})
|
||||
);
|
||||
// The control: the full role write does carry the certificate, so a
|
||||
// body without one is the narrow write and not a failure to serialise.
|
||||
let full = cert_role_request("cert", "hive-agent-a", "PEM", "hive-agent-a", &["p"])
|
||||
.request("http://store.invalid")
|
||||
.expect("builds");
|
||||
let full: serde_json::Value = serde_json::from_slice(full.body()).expect("json body");
|
||||
assert_eq!(full["certificate"], "PEM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_complete_environment_is_accepted() {
|
||||
// The control: without this, every assertion below could be passing
|
||||
|
|
|
|||
Loading…
Reference in a new issue