swarm-bao: agent certificates issued by a store-generated agent CA
An agent's store identity was signed in swarm-controller's memory by a CA a controller-host unit generated on disk, and the listener never trusted that CA. Agent leaves now come from the store itself: a `pki-agents` PKI mount whose root openbao generates internally, so the agent CA's key never exists outside the store. - swarm-bao-agent-pki (new, store host, as the bao granter): enables and tunes the mount, generates the root once (guarded on an empty issuer list, no replace branch), upserts the `swarm-agent` role (client certificates named `hive-agent-*` only, 90 days), caches the CA at /var/lib/swarm-bao-tls/agent-ca.pem and composes the listener bundle. - The listener's tls_client_ca_file is a new listener-client-ca.pem (client-ca.pem, then the agent CA). Host cert-auth roles still pin client-ca.pem, so an agent leaf satisfies no host role. swarm-bao-certs composes the same bundle before openbao starts. - openbao reads tls_client_ca_file only at start, so when the bundle changed after openbao started, swarm-bao-agent-pki restarts openbao.service in the container; under `seal = "shamir"` it prints the step instead. Once swarm-bao-certs has a cached CA, later boots start openbao with it and do not restart. - The controller policy gains exactly `update` on pki-agents/issue/swarm-agent. mint_and_verify now asks that role for the leaf (the store generates the key), writes the agent's cert-auth role pinning the issuing CA bao returned, and writes the agent's policy as render_agent alone: the hive-shared queue credential stanza is gone. - deploy.bao.agentPkiRoleName (must start `swarm-`, asserted with the other pki role names); swarm-controller gets SWARM_CONTROLLER_AGENT_PKI_MOUNT/_ROLE from the deploy.bao options. Deleted: swarm-controller-agent-ca and its options (agentCaFile, agentCaKeyFile), env, LoadCredential entries and assertion; agent_identity's Authority, rcgen signing and validity window; the rcgen and time dependencies of swarm-controller (rcgen leaves the workspace); policy::render_agent_with_queue and its tests. The CN-prefix assertion policy.rs said was owed is not: agent and host roles pin different CAs. Migration is re-creating each agent after deploy; that overwrites the stale role and policy. Closes #4756
This commit is contained in:
parent
5cd7f866f4
commit
6170e74a31
16 changed files with 894 additions and 925 deletions
|
|
@ -44,6 +44,8 @@ let
|
|||
# The store's units live inside its container, so the gates below have to
|
||||
# look there rather than at the host's service set.
|
||||
baoUnits = machine: machine.containers.swarm-bao.config.systemd.services;
|
||||
|
||||
tlsDir = "/var/lib/swarm-bao-tls";
|
||||
cases = [
|
||||
{
|
||||
# The store's seal is spread over six gates — the stanza, the
|
||||
|
|
@ -214,6 +216,71 @@ let
|
|||
name = "the store advertises a cluster address";
|
||||
ok = lib.hasPrefix "https://" ((baoSettings baoPkcs11).cluster_addr or "");
|
||||
}
|
||||
{
|
||||
# The listener trusts the agent CA through a file no cert-auth role
|
||||
# names. At least one listener verifies clients, so an empty set cannot
|
||||
# pass.
|
||||
name = "every client-verifying listener reads the listener-only bundle";
|
||||
ok =
|
||||
let
|
||||
verifying = lib.filter (l: l ? tls_client_ca_file) (
|
||||
lib.attrValues (baoSettings baoPkcs11).listener
|
||||
);
|
||||
in
|
||||
verifying != [ ]
|
||||
&& lib.all (l: l.tls_client_ca_file == "${tlsDir}/listener-client-ca.pem") verifying;
|
||||
}
|
||||
{
|
||||
# The other half of keeping agents out of host roles: those roles pin
|
||||
# the store CA alone. The positive count is the control that the text
|
||||
# searched is the one the roles are written in.
|
||||
name = "host cert-auth roles pin client-ca.pem and never the listener bundle or the agent CA";
|
||||
ok =
|
||||
let
|
||||
scripts = lib.concatStrings (
|
||||
lib.mapAttrsToList (
|
||||
n: u: lib.optionalString (lib.hasPrefix "swarm-bao-" n) (u.script or "")
|
||||
) baoPkcs11.systemd.services
|
||||
);
|
||||
in
|
||||
lib.hasInfix "certificate=@${tlsDir}/client-ca.pem" scripts
|
||||
&& !(lib.hasInfix "certificate=@${tlsDir}/listener-client-ca.pem" scripts)
|
||||
&& !(lib.hasInfix "certificate=@${tlsDir}/agent-ca.pem" scripts);
|
||||
}
|
||||
{
|
||||
# Both writers compose the bundle through one function, which refuses
|
||||
# a result that does not begin with the store CA and replaces the file
|
||||
# only when its bytes change.
|
||||
name = "both bundle writers use the one composer, which keeps the store CA first";
|
||||
ok =
|
||||
let
|
||||
s = baoPkcs11.systemd.services;
|
||||
composes =
|
||||
u:
|
||||
lib.hasInfix "compose_listener_bundle() {" u.script
|
||||
&& lib.hasInfix "\ncompose_listener_bundle\n" u.script
|
||||
&& lib.hasInfix ''cmp -s -n "$(stat -c %s ${tlsDir}/client-ca.pem)" ${tlsDir}/client-ca.pem "$tmp"'' u.script
|
||||
&& lib.hasInfix ''cmp -s "$tmp" ${tlsDir}/listener-client-ca.pem'' u.script
|
||||
&& lib.hasInfix "mktemp -p ${tlsDir} " u.script;
|
||||
in
|
||||
s ? swarm-bao-agent-pki && composes s.swarm-bao-certs && composes s.swarm-bao-agent-pki;
|
||||
}
|
||||
{
|
||||
# openbao reads its client-CA file only at start, so picking up the
|
||||
# agent CA is a restart: automatic where the store unseals itself,
|
||||
# printed where a human has to. The shamir arm is the control.
|
||||
name = "the agent PKI unit restarts openbao under pkcs11 and only prints the step under shamir";
|
||||
ok =
|
||||
let
|
||||
restart = ''systemctl --machine="$machine" restart openbao.service'';
|
||||
p = baoPkcs11.systemd.services.swarm-bao-agent-pki.script;
|
||||
sh = baoShamir.systemd.services.swarm-bao-agent-pki.script;
|
||||
in
|
||||
lib.hasInfix restart p
|
||||
&& lib.hasInfix ''if [ "$written_us" -le "$started_us" ]; then'' p
|
||||
&& !(lib.hasInfix restart sh)
|
||||
&& lib.hasInfix "# then unseal" sh;
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "bao-basics" cases
|
||||
|
|
|
|||
Loading…
Reference in a new issue