diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index b13fd5f2..9ccdf24d 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -37,13 +37,13 @@ systemctl restart swarm-bao-granter-role Check `systemctl status swarm-bao-granter-role` logs `Uploaded policy: bao-granter`, then restart the granting units that -failed while they waited, and remove the token: +failed while they waited. The granter unit deletes the token itself after a +successful run: ```bash systemctl reset-failed 'swarm-bao-*-policy.service' swarm-bao-agent-pki.service systemctl restart 'swarm-bao-*-policy.service' swarm-bao-agent-pki.service systemctl status swarm-bao-controller-policy # Uploaded policy, Data written to: auth/cert/certs/swarm-controller -rm /var/lib/swarm-bao-bootstrap/grant.token ``` ⏱️ A first attempt right after a rebuild may fail with `local node not diff --git a/docs/swarm/bao.md b/docs/swarm/bao.md index 4bc27926..37fda90b 100644 --- a/docs/swarm/bao.md +++ b/docs/swarm/bao.md @@ -43,7 +43,10 @@ TTL means a forgotten file expires rather than lingering. A sealed or unreachable store makes the unit retry every 30 s. A store that refuses the token (expired, revoked, or missing the `bao-bootstrap` policy) makes it exit 4 and stay `failed` with the one-time step in its log; mint a -fresh token or remove the file, then restart it. +fresh token or remove the file, then restart it. If the granter's own +certificate login already works, the unit logs that the bootstrap token isn't +needed and exits 0, so a stale file is harmless. A successful run deletes the +token file. After that, a new or changed `swarm-*` grant needs no operator step: the unit that writes it changes, and the deploy restarts it. A root step comes back only diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index f3a8240b..59516aee 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -3182,6 +3182,17 @@ in # revoked, or minted without that policy. if ! mounted="$(bao auth list -format=json 2>"$err")"; then if ${baoStoreUp}; then + # A dead token beside a working granter is the steady state of + # a host that finished bootstrap and kept the file. The + # granter's own login is the proof. A fresh token still takes + # the full path below, since the granter cannot read back its + # own policy to compare it. + if BAO_CLIENT_CERT=${lib.escapeShellArg baoDeploy.granterClientCertFile} \ + BAO_CLIENT_KEY=${lib.escapeShellArg baoDeploy.granterClientKeyFile} \ + bao login -method=cert -token-only >/dev/null 2>&1; then + echo "granter role already in place; bootstrap token not needed" + exit 0 + fi echo ${lib.escapeShellArg "the store is unsealed but refused the bootstrap token in ${baoDeploy.bootstrapTokenFile}: it has expired, been revoked, or does not carry the bao-bootstrap policy."} >&2 echo "one-time step, as root on this host (docs/getting-started/setup.md):" >&2 ${lib.concatMapStringsSep "\n" (l: "echo ${lib.escapeShellArg " ${l}"} >&2") granterSetupSteps} @@ -3224,6 +3235,9 @@ in display_name=${lib.escapeShellArg granterCn} \ token_ttl=15m \ token_max_ttl=15m + + # Single-use by design; the setup procedure's last step. + rm -f ${lib.escapeShellArg baoDeploy.bootstrapTokenFile} ''; }; diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index b92c2487..4535ff27 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -1112,6 +1112,28 @@ let [ "swarm-bao-granter-role" ] ++ grantingUnitNames ); } + { + # A dead token beside a working granter exits 0; one with no working + # granter still reaches exit 4; a successful run removes the token file. + # Presence controls: the controller's unit has neither the no-op line + # nor the delete. + name = "the granter's unit no-ops on a working granter login, still exits 4 without one, and deletes the token"; + ok = + let + g = baoGrantHere.systemd.services.swarm-bao-granter-role.script; + other = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; + noop = "granter role already in place; bootstrap token not needed"; + del = "rm -f /run/secrets/bao-bootstrap.token"; + before = lib.head (lib.splitString "refused the bootstrap token" g); + in + lib.hasInfix noop before + && lib.hasInfix "bao login -method=cert -token-only" before + && lib.hasInfix "BAO_CLIENT_CERT=" before + && lib.hasInfix del g + && lib.hasInfix "exit 4" g + && !(lib.hasInfix noop other) + && !(lib.hasInfix del other); + } { # Every granting unit retries a sealed or late store for a day, in the # `[Unit]` section systemd reads it from, and waits for the unit that