diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 42248604..c5b6dedf 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -62,27 +62,11 @@ authenticate until some role exists — this token is what breaks that cycle, and it's the only step that needs the root token. ```bash -# Exactly the four grants the bootstrap unit needs, and nothing else. Each was -# derived with `bao -output-policy`, which prints what a command requires -# without running it. +# A policy that can write exactly one policy, and nothing else. bao policy write swarm-bootstrap - <<'EOF' path "sys/policies/acl/swarm-controller" { capabilities = ["create", "update"] } - -# Cert auth is a mount, and nothing has created it yet: reading `sys/auth` is -# how the unit checks, and `sudo` is what enabling one costs. -path "sys/auth" { - capabilities = ["read"] -} - -path "sys/auth/cert" { - capabilities = ["create", "update", "sudo"] -} - -path "auth/cert/certs/swarm-controller" { - capabilities = ["create", "update"] -} EOF # A token holding it. `-orphan` so it outlives the session that made it. @@ -91,17 +75,7 @@ bao token create -policy=swarm-bootstrap -ttl=24h -orphan -display-name=swarm-bo Put the token's value at `services.hyperhive.deploy.bao.bootstrapTokenFile` (all-local names that path for you), then rebuild. A one-shot unit inside the -store's container reads it, writes the `swarm-controller` policy, enables the -cert auth method, and creates the `swarm-controller` role that attaches the two. - -⚠️ **None of this has been run against a live store.** Nothing in the tree has -ever authenticated to OpenBao, so treat the block above as derived rather than -exercised — the grants come from `-output-policy`, not from a swarm that came -up on them. - -⚠️ **The role it creates has nothing to present a certificate for yet.** No -deployment shape mints a leaf whose CN reads `swarm-controller`, so until one -does, the role stays provisioning that waits for a consumer. +store's container reads it and writes the `swarm-controller` policy. **Delete the file once that has run.** The unit skips when it's absent, so a host that has finished bootstrapping stops carrying the credential — and the diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index fff2de8c..f48700b6 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -150,15 +150,10 @@ let bootstrapTokenDir = if haveBootstrapToken then builtins.dirOf baoDeploy.bootstrapTokenFile else null; - # The name both ends must agree on: the cert-auth role below attaches this - # policy by spelling it the same way, and is itself named after it. + # The name both ends must agree on: whoever later gives the controller a + # cert-auth role attaches this policy by spelling it the same way. controllerPolicyName = "swarm-controller"; - # The subject the controller's certificate must carry. Cert auth matches on - # the CN, so this is an interface rather than a label: a leaf signed by the - # right CA but minted with any other subject cannot authenticate. - controllerCn = "swarm-controller"; - # Derived with `bao write -output-policy`, which short-circuits the request # and needs no server, rather than written from memory. # @@ -1004,7 +999,7 @@ in # where the store is reachable without a client certificate — which # is the point, since no role exists yet to issue one against. systemd.services.swarm-bao-controller-policy = lib.mkIf haveBootstrapToken { - description = "write the swarm controller's bao policy and cert-auth role"; + description = "write the swarm controller's bao policy"; after = [ "openbao.service" ]; wantedBy = [ "multi-user.target" ]; path = [ @@ -1039,37 +1034,6 @@ in # than failing on one that already exists. printf '%s' ${lib.escapeShellArg controllerPolicyText} | bao policy write ${lib.escapeShellArg controllerPolicyName} - - '' - + lib.optionalString (baoDeploy.clientCaFile != null) '' - - # The policy above grants paths under `auth/cert/`, and nothing - # in this tree creates that mount. Without this, the grant names - # a location that does not exist and every certificate login - # fails — the controller's own, and the per-hive ones it later - # issues against the same mount. - # - # Asked rather than attempted: `auth enable` errors on a mount - # that already exists, and recognising that would tie a rebuild - # to an error string we have never seen this store emit. - mounted="$(bao auth list -format=json)" - case "$mounted" in - *'"cert/"'*) ;; - *) bao auth enable cert ;; - esac - - # `certificate=` is the CA, so this role trusts every leaf that - # CA signed and `allowed_common_names` is the whole of what - # narrows it to one identity. ⚠️ The same CA signs each hive's - # reader leaf with CN = the hive's name, so a hive named - # `${controllerCn}` would satisfy this role. - # - # Named outside the `hive-*` namespace the policy grants, so the - # controller cannot rewrite the role that constrains it. - bao write auth/cert/certs/${lib.escapeShellArg controllerPolicyName} \ - certificate=@${tlsDir}/client-ca.pem \ - allowed_common_names=${lib.escapeShellArg controllerCn} \ - token_policies=${lib.escapeShellArg controllerPolicyName} \ - display_name=${lib.escapeShellArg controllerCn} ''; }; }; diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 0c80c52c..53f2d2fc 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -254,15 +254,6 @@ let baoGrantNoStore = hive { deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; }; - # The store and the token, with no CA to trust. `mkForce` because the PKI - # glue supplies one by default here — this is the deployment that brings its - # own certificates and has not named the authority yet, and it separates - # "the grant unit runs" from "cert auth can be set up". - baoGrantNoClientCa = hive { - deploy.bao.enable = true; - deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; - deploy.bao.clientCaFile = lib.mkForce null; - }; baoNames = machine: machine.services.hyperhive.gateway.localNames; @@ -515,37 +506,10 @@ let lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" s); } { - # The policy above grants paths under a mount nothing else creates, so - # the unit that writes the policy has to create it too — otherwise every - # certificate login fails against a path that is not there. - name = "the granting unit creates the cert auth mount and the controller's role"; - ok = - let - s = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script; - in - lib.hasInfix "bao auth enable cert" s - && lib.hasInfix "auth/cert/certs/swarm-controller" s - && lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s; - } - { - # The arm that makes the one above mean something. A role's trust anchor - # is the CA, so with none named there is nothing to write — and the - # policy write, which needs no CA, must survive that. - # - # ⚠️ Matched on the COMMANDS, not on `auth/cert/certs`: the policy text is - # embedded in this same script and grants that very path, so the shorter - # infix is present either way and the arm could never fail. - name = "with no client CA the unit still writes the policy and skips the role"; - ok = - let - s = - baoGrantNoClientCa.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script; - in - lib.hasInfix "bao policy write" s - && !(lib.hasInfix "bao auth enable cert" s) - && !(lib.hasInfix "client-ca.pem" s); - } - { + # The arm that makes the one above mean something, and the property the + # host-side half depends on: no store here, so no bind mount and no unit. + # Without it a hive that merely names a token would drag the store's + # container config into its evaluation. # Reads the vhost's rendered `root`, not the option: the UI is served # straight out of a store path, so a shim that resolves but stops # reaching the module would leave nginx pointing at the default build. @@ -583,10 +547,6 @@ let == pkgs.emptyDirectory; } { - # What makes the granting-unit cases mean something, and the property - # the host-side half depends on: no store here, so no bind mount and no - # unit. Without it a hive that merely names a token would drag the - # store's container config into its evaluation. name = "a bootstrap token on a host that runs no store grants nothing"; ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir); }