diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index e3fbbc17..52568922 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -112,8 +112,9 @@ The token file is `services.hyperhive.deploy.bao.bootstrapTokenFile`, which all-local names for you. On a store host that isn't all-local, set it and rebuild first. -`swarm-bao-granter-role` runs **on the host**. It enables the cert, approle -and oidc auth methods, writes the `bao-granter` policy, and creates the +`swarm-bao-granter-role` runs **on the host**. It enables the cert and oidc +auth methods (and disables `approle` if an older store still has it mounted), +writes the `bao-granter` policy, and creates the `bao-granter` role, which accepts the leaf `/var/lib/swarm-bao-pki/granter.pem`. Every `swarm-bao-*-policy` unit then logs in with that leaf. The controller's unit diff --git a/nix/host-modules/bao-bootstrap-policy.hcl b/nix/host-modules/bao-bootstrap-policy.hcl index 8975faf4..5085f620 100644 --- a/nix/host-modules/bao-bootstrap-policy.hcl +++ b/nix/host-modules/bao-bootstrap-policy.hcl @@ -14,7 +14,7 @@ # not grant. # The auth mounts. Reading `sys/auth` is how the unit checks, and `sudo` is -# what enabling one costs. +# what enabling or disabling one costs. path "sys/auth" { capabilities = ["read"] } @@ -23,8 +23,11 @@ path "sys/auth/cert" { capabilities = ["create", "update", "sudo"] } +# Nothing mints an approle secret_id; the bootstrap step disables the mount +# rather than enabling it. `delete` is what `bao auth disable` costs, not +# `create`/`update` (verified with `bao auth disable -output-policy`). path "sys/auth/approle" { - capabilities = ["create", "update", "sudo"] + capabilities = ["delete", "sudo"] } path "sys/auth/oidc" { diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index e3f6b8e8..20fe13db 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -2687,9 +2687,11 @@ in *) bao auth enable cert ;; esac + # Nothing mints a secret_id; disabled rather than left unmounted, + # since a store from before this change may already have it. case "$mounted" in - *'"approle/"'*) ;; - *) bao auth enable approle ;; + *'"approle/"'*) bao auth disable approle ;; + *) ;; esac # The UI's OIDC login; `swarm-bao-operator-viewer-policy` writes its diff --git a/nix/module-eval/bao-grants.nix b/nix/module-eval/bao-grants.nix index fc0821a2..cd66ed66 100644 --- a/nix/module-eval/bao-grants.nix +++ b/nix/module-eval/bao-grants.nix @@ -200,8 +200,9 @@ let # One `bao …` invocation → the path and capabilities it needs, as # `bao -output-policy` reports them. Path-specific `sudo` (bao's # root-protected paths, e.g. `pki/root` for a delete) does not follow from - # the verb, so only `auth enable` is checked for it. A verb not listed here - # needs a path no grant has, so it fails the case until it is taught. + # the verb, so only `auth enable`/`auth disable` are checked for it. A verb + # not listed here needs a path no grant has, so it fails the case until it + # is taught. baoCallNeeds = words: let @@ -234,6 +235,11 @@ let need "sys/auth" [ "read" ] else if a 0 == "auth" && a 1 == "enable" then need "sys/auth/${a 2}" (cu ++ [ "sudo" ]) + else if a 0 == "auth" && a 1 == "disable" then + need "sys/auth/${a 2}" [ + "delete" + "sudo" + ] else if a 0 == "write" then need (a 1) cu else if a 0 == "read" then @@ -1286,6 +1292,24 @@ let ] && grantFor bootstrapGrants "sys/policies/acl/swarm-controller" == null; } + { + # Nothing mints a secret_id, so the bootstrap policy's approle grant is + # for tearing the mount down, not standing it up: `delete`+`sudo` + # (verified against `bao auth disable -output-policy`), not + # `create`/`update`. The granter's unit disables an existing mount and + # never enables one. + name = "the bootstrap policy may disable approle, and the granter's unit never enables it"; + ok = + let + g = baoGrantHere.systemd.services.swarm-bao-granter-role.script; + in + (grantFor bootstrapGrants "sys/auth/approle").caps == [ + "delete" + "sudo" + ] + && lib.hasInfix "bao auth disable approle" g + && !(lib.hasInfix "bao auth enable approle" g); + } { # The controller's whole reach on any PKI mount: one role's issue # endpoint. It cannot rewrite the role, sign a CSR of its choosing or @@ -1409,9 +1433,9 @@ let && grantFor bootstrapGrants "sys/auth/oidc" != null; } { - # Asked before attempted, like the cert and approle mounts, so re-running - # the step on a store that has the mount is a no-op; listed, or the UI's - # login page shows no OIDC tab. + # Asked before attempted, like the cert mount, so re-running the step + # on a store that has the mount is a no-op; listed, or the UI's login + # page shows no OIDC tab. name = "the granter's role unit enables the oidc mount once, listed on the UI's login page"; ok = let