From de4871cab13206ec6651c7ec81375a40cad474e6 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 17 Oct 2026 15:08:02 +0200 Subject: [PATCH] swarm-bao: the agent-token checkers' cert roles issue batch tokens --- docs/swarm/secrets.md | 7 +++++++ nix/host-modules/swarm-bao.nix | 29 ++++++++++++++++++++++------- nix/module-eval/bao-grants.nix | 27 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/docs/swarm/secrets.md b/docs/swarm/secrets.md index 4f7307db..090acf8f 100644 --- a/docs/swarm/secrets.md +++ b/docs/swarm/secrets.md @@ -465,6 +465,13 @@ names the hive's own `clientCertFile` and misses a principal's pair has the build refused, by the module of the reader it missed, naming that pair — see [what a missing leaf costs](#what-a-missing-leaf-costs) below. +The roles of `swarm-nats-auth`, `swarm-logs-auth` and `swarm-metrics-auth` issue +batch tokens with a five-minute TTL (`token_type=batch`, `token_ttl=5m`). The +responders log in on every check, and the store keeps no record of a batch token, +so unauthenticated requests don't grow its token storage. Nothing can renew +or revoke a batch token. The other roles keep service tokens, because some of +their readers hold a handle for longer than five minutes. + ### What a missing leaf costs Each of the three modules asserts its own pair, on the same condition: **this host diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 39bc449a..198d7d41 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -1001,6 +1001,7 @@ let { name = "swarm-nats-auth"; cn = baoDeploy.natsAuthCommonName; + batchToken = true; policyText = readStanza "${credentialMountPath}/data/swarm/agents/+/queue" + readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.nats.clientId}/oidc/client" @@ -1052,6 +1053,7 @@ let { name = "swarm-logs-auth"; cn = baoDeploy.logsAuthCommonName; + batchToken = true; policyText = readStanza "${credentialMountPath}/data/swarm/agents/+/queue" + readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}" @@ -1069,6 +1071,7 @@ let { name = "swarm-metrics-auth"; cn = baoDeploy.metricsAuthCommonName; + batchToken = true; policyText = readStanza "${credentialMountPath}/data/swarm/agents/+/queue" + readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}" @@ -1182,13 +1185,25 @@ let # `swarm` in one — and ./swarm.nix reserves the concrete per-hive spellings # anyway, because the prefixes are operator-settable and an operator may spell # one without that substring. - readerRoleWrite = obj: '' - bao write auth/cert/certs/${lib.escapeShellArg obj.name} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg obj.cn} \ - token_policies=${lib.escapeShellArg obj.name} \ - display_name=${lib.escapeShellArg obj.cn} - ''; + # + # `batchToken = true` on an object makes its logins batch tokens, which the + # store does not persist and cannot renew or revoke. Only for a principal + # that logs in, reads and drops the token within one call: the five + # minutes outlast that read, and a leaked token dies as fast. A principal + # that holds a handle longer must not set it. + readerRoleWrite = + obj: + let + batchArgs = lib.optionalString (obj.batchToken or false + ) " \\\n token_type=batch \\\n token_ttl=5m"; + in + '' + bao write auth/cert/certs/${lib.escapeShellArg obj.name} \ + certificate=@${tlsDir}/client-ca.pem \ + allowed_common_names=${lib.escapeShellArg obj.cn} \ + token_policies=${lib.escapeShellArg obj.name} \ + display_name=${lib.escapeShellArg obj.cn}${batchArgs} + ''; # A unit that logs in as the granter and runs `body`, ordered after the # controller's unit. `after` and not `requires`: the controller's and the diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index b95ac71b..506c4fe5 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -815,6 +815,33 @@ let && lib.hasInfix "allowed_common_names=swarm-metrics-auth" s && lib.hasInfix "token_policies=swarm-metrics-auth" s; } + { + # The three agent-token checkers log in once per check; a batch token + # leaves nothing stored per request. The CI runner's role, written by the + # same helper, keeps service tokens, and still carries its policy, so a + # misspelled script name cannot make the absence pass. + name = "the agent-token checkers' roles issue batch tokens and a holder's role does not"; + ok = + let + script = unit: baoGrantHere.systemd.services.${unit}.script; + batch = + unit: role: + let + s = script unit; + in + lib.hasInfix "auth/cert/certs/${role} " s + && lib.hasInfix "token_type=batch" s + && lib.hasInfix "token_ttl=5m" s + && !(lib.hasInfix "token_max_ttl" s); + ci = script "swarm-bao-ci-runner-policy"; + in + batch "swarm-bao-nats-auth-policy" "swarm-nats-auth" + && batch "swarm-bao-logs-auth-policy" "swarm-logs-auth" + && batch "swarm-bao-metrics-auth-policy" "swarm-metrics-auth" + && lib.hasInfix "token_policies=swarm-ci-runner" ci + && !(lib.hasInfix "token_type" ci) + && !(lib.hasInfix "token_ttl" ci); + } { name = "the PKI unit signs the metrics store responder's leaf under its own subject"; ok =