stop writing and attaching per-agent and per-hive bao policies
This commit is contained in:
parent
eac7f2a7c1
commit
139007e656
1 changed files with 253 additions and 419 deletions
|
|
@ -17,10 +17,10 @@
|
|||
//! not derived from the first, so either renews without reference to the other.
|
||||
//!
|
||||
//! Four separate strings have to agree before an agent can authenticate: the
|
||||
//! policy's name, the cert-auth role's name, the certificate's common name,
|
||||
//! and the authority the role pins. That is the kind of agreement that holds
|
||||
//! in review and fails in production — a mismatch is a 403 naming none of the
|
||||
//! four — so [`mint_and_verify`] does not finish on a write. See
|
||||
//! identity entity's name, the cert-auth role's name, the certificate's common
|
||||
//! name, and the authority the role pins. That is the kind of agreement that
|
||||
//! holds in review and fails in production — a mismatch is a 403 naming none
|
||||
//! of the four — so [`mint_and_verify`] does not finish on a write. See
|
||||
//! [`read_back_as_agent`].
|
||||
//!
|
||||
//! The authority is the store's own agent CA, generated inside its agent PKI
|
||||
|
|
@ -62,15 +62,12 @@ fn agent_pki(get: impl Fn(&str) -> Option<String>) -> Result<(String, String)> {
|
|||
|
||||
/// What the cert-auth role for `agent` is written from.
|
||||
struct RoleInputs<'a> {
|
||||
/// Role name, policy name and the common name the role matches: one string.
|
||||
/// Role name and the common name the role matches: one string.
|
||||
name: String,
|
||||
/// The authority the role pins: the one that signed this very leaf.
|
||||
ca: &'a str,
|
||||
/// The policy document the role attaches.
|
||||
policy: String,
|
||||
/// The policies the role attaches by name: the agent's own, and the one every
|
||||
/// agent shares.
|
||||
attached: [String; 2],
|
||||
/// The policies the role attaches by name: the one every agent shares.
|
||||
attached: Vec<String>,
|
||||
}
|
||||
|
||||
fn role_inputs<'a>(agent: &str, credential: &'a mtls::Credential) -> Result<RoleInputs<'a>> {
|
||||
|
|
@ -78,8 +75,7 @@ fn role_inputs<'a>(agent: &str, credential: &'a mtls::Credential) -> Result<Role
|
|||
Ok(RoleInputs {
|
||||
name: name.clone(),
|
||||
ca: &credential.ca,
|
||||
policy: policy::render_agent(agent)?,
|
||||
attached: [name, policy::SHARED_AGENT_POLICY.to_owned()],
|
||||
attached: vec![policy::SHARED_AGENT_POLICY.to_owned()],
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +111,7 @@ pub(crate) fn generate_queue_secret() -> Result<String> {
|
|||
|
||||
/// Give `agent` an identity at the store, and prove it works.
|
||||
///
|
||||
/// Five store calls' worth of agreement, then the login that checks it:
|
||||
/// Six store calls' worth of agreement, then the login that checks it:
|
||||
///
|
||||
/// 1. have the agent PKI role issue a leaf whose common name is
|
||||
/// [`policy::agent_object_name`]; the store generates its key;
|
||||
|
|
@ -124,14 +120,13 @@ pub(crate) fn generate_queue_secret() -> Result<String> {
|
|||
/// 3. publish a queue secret at [`queue::agent_queue_path`] — the agent's own
|
||||
/// identity at the swarm queue, minted here so that the credential an agent
|
||||
/// presents names *it* rather than its hive;
|
||||
/// 4. write the ACL document [`policy::render_agent`] renders — read and list on
|
||||
/// this one agent's paths — and the shared [`policy::render_shared_agent`];
|
||||
/// 5. write the cert-auth role that ties the three together, attaching both
|
||||
/// policies and pinning the CA the store named as this leaf's issuer.
|
||||
/// 4. write the shared ACL document [`policy::render_shared_agent`] renders;
|
||||
/// 5. write the cert-auth role that ties the three together, attaching that
|
||||
/// policy and pinning the CA the store named as this leaf's issuer;
|
||||
/// 6. name the agent's identity entity after the agent ([`crate::agent_entity`]),
|
||||
/// which the policy reads.
|
||||
///
|
||||
/// Policy before role, for the reason `read_policy::provision` gives: the role
|
||||
/// names the policy, so the other order leaves a window in which it points at
|
||||
/// nothing.
|
||||
/// Policy before role, for the reason `read_policy::provision` gives.
|
||||
///
|
||||
/// ⚠️ **Step 3 is idempotent and steps 1–2 are not.** Re-running issues a
|
||||
/// fresh leaf, picked up on the agent's next boot, but leaves an existing
|
||||
|
|
@ -140,9 +135,10 @@ pub(crate) fn generate_queue_secret() -> Result<String> {
|
|||
/// replacing one by age is [`crate::agent_renewal`]'s.
|
||||
///
|
||||
/// # Errors
|
||||
/// Anything that stops one of those five steps, with the step named. A
|
||||
/// failure here fails the job node and nothing else — the agent is still
|
||||
/// created, without a store identity.
|
||||
/// Anything that stops steps 1–5, with the step named, or the read-back login,
|
||||
/// which is where a step 6 that did not take shows up. A failure here fails the
|
||||
/// job node and nothing else — the agent is still created, without a store
|
||||
/// identity.
|
||||
pub async fn mint_and_verify(agent: &str) -> Result<()> {
|
||||
let (mount, pki_role) = agent_pki(|k| std::env::var(k).ok())?;
|
||||
let name = policy::agent_object_name(agent)?;
|
||||
|
|
@ -201,10 +197,6 @@ pub async fn mint_and_verify(agent: &str) -> Result<()> {
|
|||
let queue_credential = wanted;
|
||||
|
||||
let inputs = role_inputs(agent, &credential)?;
|
||||
store
|
||||
.write_policy(&inputs.name, &inputs.policy)
|
||||
.await
|
||||
.with_context(|| format!("writing the read policy {}", inputs.name))?;
|
||||
store
|
||||
.write_policy(policy::SHARED_AGENT_POLICY, &policy::render_shared_agent())
|
||||
.await
|
||||
|
|
@ -220,7 +212,11 @@ pub async fn mint_and_verify(agent: &str) -> Result<()> {
|
|||
&inputs.name,
|
||||
inputs.ca,
|
||||
&inputs.name,
|
||||
&inputs.attached.each_ref().map(String::as_str),
|
||||
&inputs
|
||||
.attached
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("writing the cert-auth role {}", inputs.name))?;
|
||||
|
|
@ -297,7 +293,7 @@ pub async fn revoke_queue_credential(agent: &str) -> Result<()> {
|
|||
///
|
||||
/// Both paths, not just the certificate's, for the reason this function
|
||||
/// exists at all: the queue credential is readable only because it sits inside
|
||||
/// the stanza [`policy::render_agent`] already grants, and a policy that
|
||||
/// the stanza [`policy::render_shared_agent`] grants, and a policy that
|
||||
/// drifted from that path would otherwise fail at the agent's first connection
|
||||
/// — far from here, as a denial naming neither the policy nor the path.
|
||||
///
|
||||
|
|
@ -450,9 +446,9 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Three of the four strings that must agree, checked where this process
|
||||
/// sets them: role, policy and matched CN are one name; the pinned CA is
|
||||
/// the issuer the store reported for this leaf; the policy is the agent's
|
||||
/// own single stanza.
|
||||
/// sets them: role and matched CN are one name; the pinned CA is the issuer
|
||||
/// the store reported for this leaf; the role attaches the shared policy
|
||||
/// and no policy of the agent's own.
|
||||
#[test]
|
||||
fn the_role_pins_the_leafs_own_issuer_under_the_agents_name() {
|
||||
let credential = mtls::Credential {
|
||||
|
|
@ -468,17 +464,21 @@ mod tests {
|
|||
"the CN the leaf is issued for"
|
||||
);
|
||||
assert_eq!(inputs.ca, "AGENT-CA");
|
||||
// Only the shared policy: a policy named for the agent is not attached.
|
||||
assert_eq!(
|
||||
inputs.attached,
|
||||
["hive-agent-atlas", policy::SHARED_AGENT_POLICY],
|
||||
"the role attaches the agent's own policy and the shared one"
|
||||
[policy::SHARED_AGENT_POLICY],
|
||||
"the role attaches the shared policy and nothing else"
|
||||
);
|
||||
assert!(
|
||||
!inputs.attached.contains(&inputs.name),
|
||||
"the role must not attach a policy named for the agent"
|
||||
);
|
||||
assert_eq!(inputs.policy, policy::render_agent("atlas").expect("legal"));
|
||||
assert!(!inputs.policy.contains("swarm/hives/"), "{}", inputs.policy);
|
||||
|
||||
// The control: another agent's inputs differ in both name and grant.
|
||||
// The control: another agent's inputs differ in name, and attach the
|
||||
// same shared policy, so the check above can see a policy that is there.
|
||||
let other = role_inputs("argus", &credential).expect("legal");
|
||||
assert_ne!(other.name, inputs.name);
|
||||
assert!(!other.policy.contains("agents/atlas/"), "{}", other.policy);
|
||||
assert_eq!(other.attached, inputs.attached);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue