From 5cd7f866f4afa9f32e996212f55ccb00b9419619 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 27 Sep 2026 17:54:35 +0200 Subject: [PATCH] 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. --- nix/host-modules/swarm-bao.nix | 54 ++++++++++++++++++++++++++++++++-- nix/module-eval/bao-grants.nix | 31 ++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index ca623c93..98843109 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -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"; diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index 2a89ccf7..b2ea4f39 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -820,7 +820,7 @@ let { # The granter's grants, whole. Pinned as the full list, because an added # path or capability is exactly what a presence check misses. - name = "the granter's policy is exactly these eleven stanzas"; + name = "the granter's policy is exactly these seventeen stanzas"; ok = let cu = [ @@ -876,6 +876,30 @@ let path = "pki/root/generate/internal"; caps = cu; } + { + path = "sys/mounts/pki-agents"; + caps = cu; + } + { + path = "sys/mounts/pki-agents/tune"; + caps = cu; + } + { + path = "pki-agents/issuers"; + caps = [ "list" ]; + } + { + path = "pki-agents/cert/ca"; + caps = [ "read" ]; + } + { + path = "pki-agents/root/generate/internal"; + caps = cu; + } + { + path = "pki-agents/roles/swarm-*"; + caps = cu; + } ]; } { @@ -908,6 +932,10 @@ let "sys/policies/acl/root" "pki/issue/swarm-services" "pki/sign/swarm-services" + "pki-agents/root" + "pki-agents/issue/swarm-agent" + "pki-agents/sign/swarm-agent" + "pki-agents/sign-verbatim" "*" ] && !(lib.any ( @@ -917,6 +945,7 @@ let "sys/policies/acl/*" "auth/cert/certs/*" "pki/roles/*" + "pki-agents/roles/*" ] ) granterGrants); }