swarm-bao: grant the agent PKI mount (for #4756)

#4756 moves agent client certificates onto a PKI mount of their own,
`pki-agents`, whose root bao generates internally. The unit that sets
that mount up runs as the bao granter, and the granter's policy is only
written while #4754's one-time bootstrap token is in place. Adding these
grants after an operator has done that step would cost a second token
placement, so they go into the granter's policy here, before it.

Six stanzas: enable and tune the mount, list its issuers, read its CA,
generate its root internally, and write `swarm-*` roles on it. No root
delete or sudo: agent cert-auth roles pin that root by value, so
replacing it must not be something a deploy can do.

Adds deploy.bao.agentPkiMountPath (default `pki-agents`), which the
stanzas are rendered from. The module-eval case pinning the granter's
policy now lists all seventeen stanzas, and the "grants nothing outside"
case also refuses the agent mount's root, issue, sign and a roles/*
glob.
This commit is contained in:
atlas 2026-09-27 17:54:35 +02:00 • committed by mara
commit 5cd7f866f4
2 changed files with 81 additions and 4 deletions

View file

@ -185,10 +185,13 @@ let
# A trailing `*` in a bao ACL path is a pure string-prefix match, and an
# exact path wins over any prefix.
#
# The first three are the per-principal grants. The rest are what
# The first three are the per-principal grants. The next eight are what
# `swarm-bao-controller-policy` does besides grants: the KV and pki mounts and
# the services root. No `sys/auth`: the auth mounts are created with the
# bootstrap token by `swarm-bao-granter-role`.
# the services root. The last six set up the agent PKI mount: the mount, its
# root and the `swarm-*` role agent certificates are issued through. No
# `root` delete there: every agent's cert-auth role pins that root by value,
# so replacing it would lock every agent out. No `sys/auth`: the auth mounts
# are created with the bootstrap token by `swarm-bao-granter-role`.
#
# Piped as a shell-quoted argument like `controllerPolicyText`, so
# ../module-eval/bao-grants.nix can read it out of the unit script.
@ -236,6 +239,30 @@ let
path "${servicesPkiMountPath}/root/generate/internal" {
capabilities = ["create", "update"]
}
path "sys/mounts/${agentPkiMountPath}" {
capabilities = ["create", "update"]
}
path "sys/mounts/${agentPkiMountPath}/tune" {
capabilities = ["create", "update"]
}
path "${agentPkiMountPath}/issuers" {
capabilities = ["list"]
}
path "${agentPkiMountPath}/cert/ca" {
capabilities = ["read"]
}
path "${agentPkiMountPath}/root/generate/internal" {
capabilities = ["create", "update"]
}
path "${agentPkiMountPath}/roles/swarm-*" {
capabilities = ["create", "update"]
}
'';
# What a granting unit prints when the store refuses the granter: the
@ -473,6 +500,13 @@ let
# and has to spell it the same way.
servicesPkiMountPath = baoDeploy.servicesPkiMountPath;
# The PKI mount agent client certificates are issued from. Its root is
# generated inside the store, so the agent CA's key never exists outside it.
# A mount of its own because the services mount holds exactly one issuer; a
# root apart from ./glue-bao-tls.nix's CA because that is what keeps an
# agent's certificate from satisfying any host role.
agentPkiMountPath = baoDeploy.agentPkiMountPath;
# Subject of the root generated into that mount. A label for a human reading
# a chain, not an identity anything authenticates against — same fall-through
# ./swarm-ca.nix:29-37 uses, and for the same reason: a hive that has set
@ -1221,6 +1255,20 @@ in
'';
};
agentPkiMountPath = lib.mkOption {
type = lib.types.str;
default = "pki-agents";
description = ''
Mount path of the PKI engine agent client certificates are issued
from. Its root is generated inside the store and its key never leaves
it.
An option rather than a literal because the store host sets the mount
up while swarm-controller, possibly on another host, issues through
it: both have to spell it identically.
'';
};
servicesPkiRoleName = lib.mkOption {
type = lib.types.str;
default = "swarm-services";