# `checks.module-eval-bao-grants` — see ./lib.nix for the shared # rationale (why this suite exists, naming convention, "evaluates # not executes"). { pkgs, lib, self, nixosSystem, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) hive runGroup ; # The store, plus a placed bootstrap token: the only shape in which the # granter's own role can be written at all. baoGrantHere = hive { deploy.bao.enable = true; deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; }; # The credential without the store. Writing the first grant is a store-side # operation, so a host holding only the token has nothing to do — and this # is the arm that separates "an operator placed a token" from "this box can # act on it". baoGrantNoStore = hive { deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; }; # The store with no bootstrap token: the steady state once the granter is set # up, and the state of a store host that has never named one. baoGranterNoToken = hive { deploy.bao.enable = true; }; # The store with the granter's pair taken away: the deployment that writes # its grants some other way. baoGranterOptOut = hive { deploy.bao.enable = true; deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; deploy.bao.granterClientCertFile = lib.mkForce null; deploy.bao.granterClientKeyFile = lib.mkForce null; }; # A pki role the granter's `roles/swarm-*` does not reach. baoGranterOddPkiRole = hive { deploy.bao.enable = true; deploy.bao.natsPkiRoleName = "queue"; }; # 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, in which nothing can # log in as the granter. baoGrantNoClientCa = hive { deploy.bao.enable = true; deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; deploy.bao.clientCaFile = lib.mkForce null; }; # The store plus every one of the four readers that used to log in as the # hive. One fixture rather than four: the claim they are four *separate* # principals is only testable where all four render at once — that is the # deployment in which two of them sharing a leaf would be invisible. baoGrantWithConsumers = hive { deploy.bao.enable = true; deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; deploy.matrix.enable = true; deploy.grafana.enable = true; deploy.swarm-otel.enable = true; }; # The store with none of the four readers beside it. Grafana, the collector and # the homeserver are simply off; the queue reader renders on any host holding # its leaf, which ./glue-bao-tls.nix mints here, so that leaf is taken away. baoGrantNoReaders = hive { deploy.bao.enable = true; deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; deploy.bao.queueAgentClientCertFile = lib.mkForce null; deploy.bao.queueAgentClientKeyFile = lib.mkForce null; }; # The store with the forwarder's own pair taken away. The forwarder renders # wherever the store does, so this is the deployment the assertion refuses. baoNoForwarderIdentity = hive { deploy.bao.enable = true; deploy.bao.forwarderOidcClientCertFile = lib.mkForce null; deploy.bao.forwarderOidcClientKeyFile = lib.mkForce null; }; # All four readers against a store they do not run, each with a leaf placed # by hand. The deployment in which there is no local policy unit to wait for. baoRemoteReaders = hive { deploy.matrix.enable = true; deploy.grafana.enable = true; deploy.swarm-otel.enable = true; deploy.bao.clientCertFile = "/etc/pki/bao-client.pem"; deploy.bao.clientKeyFile = "/etc/pki/bao-client-key.pem"; deploy.bao.matrixTokenClientCertFile = "/etc/pki/bao-matrix-token.pem"; deploy.bao.matrixTokenClientKeyFile = "/etc/pki/bao-matrix-token-key.pem"; deploy.bao.queueAgentClientCertFile = "/etc/pki/bao-queue-agent.pem"; deploy.bao.queueAgentClientKeyFile = "/etc/pki/bao-queue-agent-key.pem"; deploy.bao.grafanaOidcClientCertFile = "/etc/pki/bao-grafana-oidc.pem"; deploy.bao.grafanaOidcClientKeyFile = "/etc/pki/bao-grafana-oidc-key.pem"; deploy.bao.otelOidcClientCertFile = "/etc/pki/bao-otel-oidc.pem"; deploy.bao.otelOidcClientKeyFile = "/etc/pki/bao-otel-oidc-key.pem"; }; # The four readers ./glue-bao-readers-policy-order.nix orders after their # policy units. policyReaders = [ "swarm-bao-matrix-token" "swarm-bao-queue-agent" "swarm-bao-grafana-oidc" "swarm-bao-otel-oidc" ]; # Two credentials write grants, and each is checked against what the units # holding it actually call. The bootstrap token's policy is read from the # file the operator writes it from (../../docs/getting-started/setup.md # points there); the granter's from the unit that writes it. Units are found # by the credential they read rather than by name, so a new one is checked # without anyone listing it here. bootstrapTokenFile = "/run/secrets/bao-bootstrap.token"; # The READ, not the path: every granting unit prints the path in the # one-time step it shows when the granter is refused. bootstrapUnits = lib.filterAttrs ( _: u: lib.hasInfix "cat ${lib.escapeShellArg bootstrapTokenFile}" u.script ) baoGrantWithConsumers.systemd.services; # The pair ./glue-bao-tls.nix defaults on a store host. granterCertFile = "/var/lib/swarm-bao-pki/granter.pem"; granterKeyFile = "/var/lib/swarm-bao-pki/granter-key.pem"; granterUnits = lib.filterAttrs ( _: u: (u.environment.BAO_CLIENT_CERT or null) == granterCertFile ) baoGrantWithConsumers.systemd.services; # The ten units that write a `swarm-*` grant, by name, for the discovery # control below. grantingUnitNames = [ "swarm-bao-controller-policy" "swarm-bao-secret-publisher-policy" "swarm-bao-matrix-ctl-policy" "swarm-bao-matrix-token-policy" "swarm-bao-queue-agent-policy" "swarm-bao-grafana-oidc-policy" "swarm-bao-otel-oidc-policy" "swarm-bao-forwarder-oidc-policy" "swarm-bao-services-issuer-policy" "swarm-bao-nats-tls-policy" ]; # Comment lines dropped first: both the HCL and the scripts explain # themselves in prose that names paths and `bao` commands. codeLines = text: lib.filter (l: builtins.match "[[:space:]]*#.*" l == null) (lib.splitString "\n" text); bootstrapPolicyText = lib.concatStringsSep "\n" ( codeLines (builtins.readFile ../host-modules/bao-bootstrap-policy.hcl) ); # The granter's HCL is the only policy text in the unit that writes it. granterPolicyText = baoGrantHere.systemd.services.swarm-bao-granter-role.script; matches = re: text: lib.filter lib.isList (builtins.split re text); grantsIn = text: map (m: { path = lib.elemAt m 0; caps = map lib.head (matches ''"([a-z]+)"'' (lib.elemAt m 1)); }) ( matches ''path "([^"]+)"[[:space:]]*[{][[:space:]]*capabilities[[:space:]]*=[[:space:]]*[[]([a-z", ]*)'' text ); bootstrapGrants = grantsIn bootstrapPolicyText; granterGrants = grantsIn granterPolicyText; # 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. baoCallNeeds = words: let flags = lib.filter (lib.hasPrefix "-") words; args = lib.filter (w: !(lib.hasPrefix "-" w) && w != "\\") words; a = i: if i < lib.length args then lib.elemAt args i else ""; need = path: caps: { inherit path caps; call = lib.concatStringsSep " " words; }; cu = [ "create" "update" ]; in # A login and a seal-status check are unauthenticated: no policy grants them. if a 0 == "login" || a 0 == "status" then null else if a 0 == "policy" && a 1 == "write" then need "sys/policies/acl/${a 2}" cu else if a 0 == "secrets" && a 1 == "list" then need "sys/mounts" [ "read" ] else if a 0 == "secrets" && a 1 == "enable" then need "sys/mounts/${lib.removePrefix "-path=" (lib.findFirst (lib.hasPrefix "-path=") "-path=${a 2}" flags)}" cu else if a 0 == "secrets" && a 1 == "tune" then need "sys/mounts/${a 2}/tune" cu else if a 0 == "auth" && a 1 == "list" then need "sys/auth" [ "read" ] else if a 0 == "auth" && a 1 == "enable" then need "sys/auth/${a 2}" (cu ++ [ "sudo" ]) else if a 0 == "write" then need (a 1) cu else if a 0 == "read" then need (a 1) [ "read" ] else if a 0 == "list" then need (a 1) [ "list" ] else if a 0 == "delete" then need (a 1) [ "delete" ] else need "unrecognised call" [ ]; baoCalls = script: lib.filter (n: n != null) ( map ( inv: baoCallNeeds (lib.filter (w: w != "") (lib.splitString " " (lib.replaceStrings [ "'" ] [ "" ] inv))) ) ( lib.concatMap (l: map (m: lib.elemAt m 1) (matches "(^[[:space:]]*|[$][(]|[)] )bao ([^|;)]*)" l)) ( codeLines script ) ) ); # bao's own rule (vault/policy/acl.go): an exact path wins, otherwise the # longest glob prefix, and a trailing `*` is a plain string prefix. grantFor = grants: path: let exact = lib.filter (g: g.path == path) grants; globs = lib.filter ( g: lib.hasSuffix "*" g.path && lib.hasPrefix (lib.removeSuffix "*" g.path) path ) grants; in if exact != [ ] then lib.head exact else lib.foldl' ( best: g: if best == null || lib.stringLength g.path > lib.stringLength best.path then g else best ) null globs; ungranted = grants: units: lib.concatLists ( lib.mapAttrsToList ( unit: u: map (n: "${unit}: `bao ${n.call}` needs ${n.path} [${toString n.caps}]") ( lib.filter ( n: let g = grantFor grants n.path; in g == null || !(lib.all (c: lib.elem c g.caps) n.caps) ) (baoCalls u.script) ) ) units ); bootstrapUngranted = ungranted bootstrapGrants bootstrapUnits; granterUngranted = ungranted granterGrants granterUnits; cases = [ { # Reads the rendered unit on the HOST, which is where the write happens: # every API listener demands a client certificate, and the host is the # side that has one. name = "a store host renders the granting unit on the host, logging in as the granter"; ok = let u = baoGrantHere.systemd.services.swarm-bao-controller-policy; in u.environment.BAO_CLIENT_CERT == granterCertFile && u.environment.BAO_CLIENT_KEY == granterKeyFile && lib.hasInfix "bao login -method=cert -token-only" u.script && !(u.unitConfig ? ConditionPathExists); } { # The move is the fix, so pin the side it landed on: in the container it # had no identity to open a connection with, and no address that resolved # to the store from its own netns. name = "the granting unit is not rendered inside the store's container"; ok = !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-controller-policy); } { # `StartLimit*` are `[Unit]` settings that systemd ignores under # `[Service]`, so a bound written into `serviceConfig` renders, deploys # and does nothing. Asserted where nixpkgs puts it rather than where it # was written. The values are pinned because they are the bound: under # `shamir` a human unseals by hand, and anything shorter than a day gives # up first — `start-limit-hit` does not self-heal. name = "the granting unit's start limit lands in [Unit], not [Service]"; ok = let u = baoGrantHere.systemd.services.swarm-bao-controller-policy; in toString u.unitConfig.StartLimitBurst == "2880" && toString u.unitConfig.StartLimitIntervalSec == "90000" && !(u.serviceConfig ? StartLimitBurst); } { # The grants themselves, and the `hive-` prefix is the whole point: # without it the controller can rewrite the policy that constrains it, # which is a privilege escalation that renders, deploys and looks fine. # Readable here only because the HCL is piped as an argument rather than # written to a store path. name = "the controller's bao grants cannot reach the policy that constrains it"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; in lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" s); } { # Same host-side reasoning as the controller's granting unit above: the # write needs a client certificate and the host is the side that has one. name = "a store host renders the publisher's granting unit too, logging in as the granter"; ok = let u = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy; in u.environment.BAO_CLIENT_CERT == granterCertFile && lib.hasInfix "swarm-secret-publisher" u.script; } { # The control for the case above, and the same one the controller's unit # has: rendered on the host means NOT rendered in the container, where it # would have neither an identity nor a route to the store. name = "the publisher's granting unit is not rendered inside the store's container"; ok = !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-secret-publisher-policy); } { # The whole point of a second principal. The two prefixes it publishes to # and not `swarm/`, so it cannot touch an agent's credentials; and no # `read`, so a unit whose job is copying a file cannot recover what is # already there. Pinned as the full capability list per prefix, because an # added capability is exactly what a presence check misses. name = "the publisher's grant is write-only and reaches the hive and service prefixes alone"; ok = let s = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.script; in lib.hasInfix "path \"secret/data/swarm/hives/*\" {\n capabilities = [\"create\", \"update\"]" s && lib.hasInfix "path \"secret/data/swarm/services/*\" {\n capabilities = [\"create\", \"update\"]" s && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/*" s) && !(lib.hasInfix "sys/policies/acl" s); } { # The ordering is load-bearing and invisible at runtime: the controller's # unit creates the KV and cert-auth mounts this one writes into, so # without it a cold boot races and fails with "route entry not found", # which names neither unit. name = "the publisher's granting unit is ordered after the one that creates the mounts"; ok = lib.elem "swarm-bao-controller-policy.service" ( baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy.after ); } { # The third principal's grant, and the narrowest of the three: ONE path, # spelled to the leaf. The negative arms are the property — a homeserver # is not entitled to overwrite Grafana's OIDC client, so widening this to # the `services/` prefix the publisher holds would be a real loss even # though it would read as tidier. # # ⚠️ `hives` is PLURAL, because the path segment comes from # `Kind::Hive`'s strum serialisation and not from `Kind::label`, which # renders the singular for error text. The singular spelling evaluates, # deploys, and 403s every read with "permission denied" and nothing else. # # 🩸 The hive NAME in the middle is the per-hive half of this credential: # the token used to be one swarm-wide value under `services/matrix/`, # which every hive's own policy granted read on. The negative arms below # are what keep it from drifting back — neither the `services/*` tree nor # a `hives/*` wildcard may appear, since either one hands matrix-ctl (or # a hive) reach beyond the single leaf it owns. # # The one other leaf is the swarm appservice token, which matrix-ctl # mints and publishes for the controller. Counted, so a third stanza # fails rather than riding along beside two correct ones. name = "matrix-ctl's grant is one hive's sender token and the swarm appservice token, nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script; in lib.hasInfix "path \"secret/data/swarm/hives/h1/matrix/sender-token\" {" s && lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"create\", \"update\", \"read\"]\n}" s && lib.length (lib.splitString "path \"" s) == 3 && !(lib.hasInfix "secret/data/swarm/services" s) && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/hives/*" s) && !(lib.hasInfix "sys/policies/acl" s); } { # 🩸 `read` is load-bearing here, and the publisher — the one sibling # that still has no `read` — shows what its absence costs. matrix-ctl's # first act is to read this path back and stop if something is there — # that read IS "and only once", so without the capability every container # restart would mint a second access token and invalidate the hive's. # (The controller holds `read` for the same idempotency reason, on the # agent prefix.) name = "matrix-ctl may read back the one path it writes"; ok = let s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script; in lib.hasInfix "capabilities = [\"create\", \"update\", \"read\"]" s && lib.hasInfix "auth/cert/certs/swarm-matrix-ctl" s && lib.hasInfix "allowed_common_names=swarm-matrix-ctl" s; } { # Same two controls its siblings carry: ordered after the unit that makes # the mounts it writes into, and rendered on the HOST rather than inside # the store's container, where it would have neither an identity nor a # route to the store. name = "matrix-ctl's granting unit is ordered after the mounts and rendered on the host"; ok = lib.elem "swarm-bao-controller-policy.service" ( baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.after ) && !(baoGrantHere.containers.swarm-bao.config.systemd.services ? swarm-bao-matrix-ctl-policy); } # ── the four readers that used to share the hive's own leaf ────────────── # # 🩸 Until this split all four presented `deploy.bao.clientCertFile`, whose # policy grants read on `swarm/agents/*`, `swarm/hives//*` AND # `swarm/services/*`. Four principals behind one certificate are one # principal to bao, so the only expressible grant was the union: the unit # fetching Grafana's OIDC secret could fetch every agent credential in the # swarm. # # Every one of these cases carries the same three negative arms, and they # are the deliverable rather than decoration — a positive arm alone passes # just as well when the other two stanzas are still there beside it. The # arms pin what each principal must NOT reach, so a later widening fails # here instead of being noticed in a store. { name = "the matrix-token reader's grant is one hive's appservice token and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-matrix-token-policy.script; in lib.hasInfix "path \"secret/data/swarm/hives/h1/matrix/appservice-token\" {" s && lib.hasInfix "capabilities = [\"read\"]" s # The three stanzas the hive's own leaf carried, none of which this # principal needs: every agent's credential, every service's OIDC # client, and the rest of its own hive's tree — including the queue # credential its sibling reader fetches. && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/services" s) && !(lib.hasInfix "secret/data/swarm/hives/h1/*" s) && !(lib.hasInfix "secret/data/swarm/hives/h1/queue" s) # A `hives/*` wildcard would serve every hive from one role and let any # hive read any other's token — the reach this split exists to remove, # not to create. && !(lib.hasInfix "secret/data/swarm/hives/*" s) # Nothing may rewrite the policy constraining it, for the reason the # controller's own `hive-*` narrowing above gives. && !(lib.hasInfix "sys/policies/acl" s); } { name = "the queue-credential reader's grant is one hive's queue credential and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-queue-agent-policy.script; in lib.hasInfix "path \"secret/data/swarm/hives/h1/queue/agent\" {" s && lib.hasInfix "capabilities = [\"read\"]" s && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/services" s) && !(lib.hasInfix "secret/data/swarm/hives/h1/*" s) && !(lib.hasInfix "secret/data/swarm/hives/h1/matrix" s) && !(lib.hasInfix "secret/data/swarm/hives/*" s) && !(lib.hasInfix "sys/policies/acl" s); } { # ⚠️ The client id is the path segment, so the negative arm that matters # for this one is the OTHER service's: `services/*` would have granted # both, and the two are separate principals precisely because a # dashboard is not entitled to a collector's credential. name = "the Grafana OIDC reader's grant is Grafana's own client secret and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-grafana-oidc-policy.script; in lib.hasInfix "path \"secret/data/swarm/services/swarm-grafana/oidc/client\" {" s && lib.hasInfix "capabilities = [\"read\"]" s && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/hives" s) && !(lib.hasInfix "secret/data/swarm/services/*" s) && !(lib.hasInfix "swarm-collector" s) && !(lib.hasInfix "sys/policies/acl" s); } { # The mirror of the case above, and the arm naming `swarm-grafana` is why # these are two principals rather than one `services/*` grant shared. name = "the collector OIDC reader's grant is the collector's own client secret and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-otel-oidc-policy.script; in lib.hasInfix "path \"secret/data/swarm/services/swarm-collector/oidc/client\" {" s && lib.hasInfix "capabilities = [\"read\"]" s && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/hives" s) && !(lib.hasInfix "secret/data/swarm/services/*" s) && !(lib.hasInfix "swarm-grafana" s) && !(lib.hasInfix "sys/policies/acl" s); } { # The fifth, and the one that stayed on the hive's leaf longest: the # store's own forwarder. Its client id is `swarm-bao-collector`, so the # arm naming `swarm-collector/` is the swarm collector's secret, which # this principal is not entitled to. name = "the store forwarder's OIDC reader's grant is its own client secret and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-forwarder-oidc-policy.script; in lib.hasInfix "path \"secret/data/swarm/services/swarm-bao-collector/oidc/client\" {" s && lib.hasInfix "capabilities = [\"read\"]" s && lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 1 && !(lib.hasInfix "secret/data/swarm/agents" s) && !(lib.hasInfix "secret/data/swarm/hives" s) && !(lib.hasInfix "secret/data/swarm/services/*" s) && !(lib.hasInfix "services/swarm-collector/" s) && !(lib.hasInfix "swarm-grafana" s) && !(lib.hasInfix "sys/policies/acl" s); } { # 🩸 The half that makes the policies above bind: a policy grants only # through a token that carries it, and a token is minted by a cert-auth # role matching a CN. Four distinct subjects is the whole mechanism — one # subject for four readers is one principal however the policies read. # # The per-hive subjects carry the hive name because their paths do; the # two service subjects do not, because an OIDC client is registered once # per swarm. Pinned so neither shape is tidied into the other. name = "each of the five readers logs in under a subject of its own"; ok = let subjectOf = unit: role: cn: let s = baoGrantHere.systemd.services.${unit}.script; in lib.hasInfix "auth/cert/certs/${role}" s && lib.hasInfix "allowed_common_names=${cn}" s && lib.hasInfix "token_policies=${role}" s # Outside the `hive-*` namespace the controller may rewrite, for # the reason the three service principals above state. && !(lib.hasInfix "auth/cert/certs/hive-" s); in subjectOf "swarm-bao-matrix-token-policy" "swarm-matrix-token-h1" "swarm-bao-matrix-token-h1" && subjectOf "swarm-bao-queue-agent-policy" "swarm-queue-agent-h1" "swarm-bao-queue-agent-h1" && subjectOf "swarm-bao-grafana-oidc-policy" "swarm-grafana-oidc" "swarm-bao-grafana-oidc" && subjectOf "swarm-bao-otel-oidc-policy" "swarm-otel-oidc" "swarm-bao-otel-oidc" && subjectOf "swarm-bao-forwarder-oidc-policy" "swarm-forwarder-oidc" "swarm-bao-forwarder-oidc"; } { # 🩸 The consuming side, and the arm that would catch the regression that # costs the most: a unit repointed back at `deploy.bao.clientCertFile` # evaluates, deploys and logs in — and silently restores the union grant, # because bao would again see one principal. Nothing about the policies # above would look wrong. # # Each pair is asserted whole: a certificate with no key authenticates # nothing, so a half-set pair is a reader that does not render. name = "each of the five readers presents its own leaf, never the hive's"; ok = let b = baoGrantWithConsumers.services.hyperhive.deploy.bao; hiveLeaf = [ b.clientCertFile b.clientKeyFile ]; own = [ b.matrixTokenClientCertFile b.matrixTokenClientKeyFile b.queueAgentClientCertFile b.queueAgentClientKeyFile b.grafanaOidcClientCertFile b.grafanaOidcClientKeyFile b.otelOidcClientCertFile b.otelOidcClientKeyFile b.forwarderOidcClientCertFile b.forwarderOidcClientKeyFile ]; envOf = unit: baoGrantWithConsumers.systemd.services.${unit}.environment; presents = unit: cert: key: (envOf unit).BAO_CLIENT_CERT == cert && (envOf unit).BAO_CLIENT_KEY == key; in lib.all (p: p != null) own && !(lib.any (p: lib.elem p hiveLeaf) own) && lib.length (lib.unique own) == lib.length own && presents "swarm-bao-matrix-token" b.matrixTokenClientCertFile b.matrixTokenClientKeyFile && presents "swarm-bao-queue-agent" b.queueAgentClientCertFile b.queueAgentClientKeyFile && presents "swarm-bao-grafana-oidc" b.grafanaOidcClientCertFile b.grafanaOidcClientKeyFile && presents "swarm-bao-otel-oidc" b.otelOidcClientCertFile b.otelOidcClientKeyFile && presents "swarm-bao-forwarder-oidc" b.forwarderOidcClientCertFile b.forwarderOidcClientKeyFile; } { # The minting side of the same claim. A role matching a subject nothing # signs is a reader that cannot log in, so the leaves and the roles have # to be asserted against each other — and the two per-hive leaves carry # THIS host's hive name, which is what makes one hive's leaf useless # against another hive's role. name = "the PKI unit signs a leaf per reader, each under that reader's own subject"; ok = let s = baoGrantHere.systemd.services.swarm-bao-pki.script; in # The basename and the subject are matched separately: `signLeaf` takes # them as consecutive arguments across a `\` continuation, so one # literal spanning both would pin this file's line wrapping rather than # the pairing it means to. lib.all (lib.flip lib.hasInfix s) [ "/matrix-token.pem ]" "swarm-bao-matrix-token-h1 \"\" clientAuth" "/queue-agent.pem ]" "swarm-bao-queue-agent-h1 \"\" clientAuth" "/grafana-oidc.pem ]" "swarm-bao-grafana-oidc \"\" clientAuth" "/otel-oidc.pem ]" "swarm-bao-otel-oidc \"\" clientAuth" "/forwarder-oidc.pem ]" "swarm-bao-forwarder-oidc \"\" clientAuth" ]; } { # The absence arm: with no client CA there is no trust anchor, so no # role can be written and nothing can log in as the granter. The units # are gone, so the deployment has to say so itself. name = "with no client CA no granting unit renders, and the deployment warns"; ok = let s = baoGrantNoClientCa.systemd.services; in lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ]) && lib.any (lib.hasInfix "services.hyperhive.deploy.bao.clientCaFile is null") baoGrantNoClientCa.warnings # The control: a store with a CA does not warn. && !(lib.any (lib.hasInfix "clientCaFile is null") baoGrantHere.warnings); } { # Same control the three service principals carry: the write needs a # client certificate and the host is the side that has one, so a unit # rendered inside the store's container would have neither an identity # nor a route. Plus the ordering that makes the mounts exist first. name = "the five readers' granting units are ordered after the mounts and rendered on the host"; ok = let units = [ "swarm-bao-matrix-token-policy" "swarm-bao-queue-agent-policy" "swarm-bao-grafana-oidc-policy" "swarm-bao-otel-oidc-policy" "swarm-bao-forwarder-oidc-policy" ]; in lib.all ( unit: lib.elem "swarm-bao-controller-policy.service" baoGrantHere.systemd.services.${unit}.after && !(baoGrantHere.containers.swarm-bao.config.systemd.services ? ${unit}) ) units; } { # The other end of those units: each reader logs in against the role its # own policy unit writes, so it has to wait for that unit. Ordering and # never a requirement: a failed policy unit still counts as done, and the # reader's own retries carry it past that. # # The forwarder is listed apart from `policyReaders`: it renders wherever # the store does, so it is never absent on a store host and never present # on a remote one, and the two cases below would fail on it for that. name = "each of the five readers is ordered after the unit writing its role"; ok = let s = baoGrantWithConsumers.systemd.services; waitsFor = reader: let policy = "${reader}-policy.service"; in lib.elem policy s.${reader}.after && lib.elem policy s.${reader}.wants && !(lib.elem policy s.${reader}.requires); in lib.all waitsFor (policyReaders ++ [ "swarm-bao-forwarder-oidc" ]); } { # The ordering is set apart from each reader's own definition, so it can # define a reader by itself: `after` on a unit nothing else declares is a # unit with no ExecStart. On the store's host without the readers, none # of the four may exist. name = "a store host without the readers gains no reader unit from their ordering"; ok = lib.all (reader: !(baoGrantNoReaders.systemd.services ? ${reader})) policyReaders; } { # Where the store is remote there is no policy unit here to wait for, so # the readers render as they did before the ordering existed. name = "a reader whose store is remote is not ordered after a policy unit"; ok = let s = baoRemoteReaders.systemd.services; unordered = reader: let policy = "${reader}-policy.service"; in s ? ${reader} && !(lib.elem policy s.${reader}.after) && !(lib.elem policy s.${reader}.wants); in lib.all unordered policyReaders; } { # A store host without the granter's pair writes its grants some other # way, so none of the ten units may exist. Without this arm # `lib.mkIf haveGranter` could be dropped from any of them and every other # case here would still pass. name = "without the granter's pair none of the ten granting units render"; ok = let s = baoGranterOptOut.systemd.services; in lib.all (unit: !(s ? ${unit})) (grantingUnitNames ++ [ "swarm-bao-granter-role" ]) # The control: the same store with the pair renders all ten. && lib.all (unit: baoGrantHere.systemd.services ? ${unit}) grantingUnitNames; } { # 🩸 What replaced the silent skip. With no bootstrap token the ten still # render, and a refused granter fails them with the step that fixes it. # A store host that never named a token is told to name one, since the # unit that sets the granter up renders only where it has. name = "a store host without a bootstrap token renders the ten, each failing loudly with the one-time step"; ok = let s = baoGranterNoToken.systemd.services; loud = unit: s ? ${unit} && lib.hasInfix "bao policy write bao-bootstrap /etc/hyperhive/bao-bootstrap-policy.hcl" s.${unit}.script && lib.hasInfix "set services.hyperhive.deploy.bao.bootstrapTokenFile" s.${unit}.script && lib.hasInfix "exit 1" s.${unit}.script; in lib.all loud grantingUnitNames && !(s ? swarm-bao-granter-role); } { # Where the token is named, the step names the file to put it in and the # unit to restart. name = "with a bootstrap token named, the one-time step places it and restarts the granter's unit"; ok = lib.all ( unit: let sc = baoGrantHere.systemd.services.${unit}.script; in lib.hasInfix "install -D -m 0600 /dev/stdin /run/secrets/bao-bootstrap.token" sc && lib.hasInfix "systemctl restart swarm-bao-granter-role" sc ) grantingUnitNames; } { # 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 # mints the granter's leaf. name = "each granting unit requires the PKI unit and retries 2880 times at 30s"; ok = lib.all ( unit: let u = baoGrantHere.systemd.services.${unit}; in lib.elem "swarm-bao-pki.service" u.requires && lib.elem "swarm-bao-pki.service" u.after && lib.elem "swarm-bao-granter-role.service" u.after && !(lib.elem "swarm-bao-granter-role.service" (u.requires ++ u.wants)) && toString u.unitConfig.StartLimitBurst == "2880" && toString u.unitConfig.StartLimitIntervalSec == "90000" && toString u.serviceConfig.RestartSec == "30" && u.serviceConfig.Restart == "on-failure" ) grantingUnitNames; } { # The only unit left acting with the token, so the only one that may # skip on it. name = "no unit but the granter's role reads the bootstrap token or skips on it"; ok = lib.attrNames bootstrapUnits == [ "swarm-bao-granter-role" ] && lib.all (u: !(u.unitConfig ? ConditionPathExists)) (lib.attrValues granterUnits) && baoGrantHere.systemd.services.swarm-bao-granter-role.unitConfig.ConditionPathExists == bootstrapTokenFile; } { # 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 seventeen stanzas"; ok = let cu = [ "create" "update" ]; in granterGrants == [ { path = "sys/policies/acl/swarm-*"; caps = cu; } { path = "auth/cert/certs/swarm-*"; caps = cu; } { path = "pki/roles/swarm-*"; caps = cu; } { path = "sys/mounts"; caps = [ "read" ]; } { path = "sys/mounts/secret"; caps = cu; } { path = "sys/mounts/pki"; caps = cu; } { path = "sys/mounts/pki/tune"; caps = cu; } { path = "pki/issuers"; caps = [ "list" ]; } { path = "pki/cert/ca"; caps = [ "read" ]; } { path = "pki/root"; caps = [ "delete" "sudo" ]; } { 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; } ]; } { # Neither its own policy and role nor the bootstrap policy may be # reachable, or the granter could rewrite what constrains it and what the # next bootstrap token carries. name = "the granter cannot reach the policy or role that constrains it, nor the bootstrap policy"; ok = lib.all (p: grantFor granterGrants p == null) [ "sys/policies/acl/bao-granter" "auth/cert/certs/bao-granter" "sys/policies/acl/bao-bootstrap" ]; } { # Outside `swarm-*` and the store's own mounts it holds nothing: no # hive's policy or role, no auth mount, no token, no secret. name = "the granter grants nothing outside swarm-* and the store's own mounts"; ok = lib.all (p: grantFor granterGrants p == null) [ "sys/policies/acl/hive-x" "auth/cert/certs/hive-x" "sys/auth" "sys/auth/cert" "sys/auth/x" "auth/token/create" "auth/token/create-orphan" "secret/data/x" "secret/data/swarm/agents/x/queue" "sys/policies/acl/x" "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 ( g: lib.elem g.path [ "*" "sys/policies/acl/*" "auth/cert/certs/*" "pki/roles/*" "pki-agents/roles/*" ] ) granterGrants); } { # Its names sit outside both globs that write grants — its own # `swarm-*` and the controller's `hive-*`. name = "the granter's own names are outside swarm-* and hive-*"; ok = let sc = baoGrantHere.systemd.services.swarm-bao-granter-role.script; cn = baoGrantHere.services.hyperhive.deploy.bao.granterCommonName; in lib.hasInfix "bao policy write bao-granter -" sc && lib.hasInfix "auth/cert/certs/bao-granter" sc && lib.hasInfix "token_policies=bao-granter" sc && lib.hasInfix "token_ttl=15m" sc && !(lib.hasPrefix "swarm-" cn) && !(lib.hasPrefix "hive-" cn); } { # The other principals are what they were: no unit but the granter's own # hands its policy to a role, and none of them logs in as it. name = "no other principal gains the granter's policy"; ok = lib.all (u: !(lib.hasInfix "token_policies=bao-granter" u.script)) ( lib.attrValues (lib.removeAttrs baoGrantWithConsumers.systemd.services [ "swarm-bao-granter-role" ]) ) && lib.all (u: (u.environment.BAO_CLIENT_CERT or null) != granterCertFile) ( lib.attrValues (lib.removeAttrs baoGrantWithConsumers.systemd.services grantingUnitNames) ); } { # The minting side: a role matching a subject nothing signs is a # granter that cannot log in. name = "the PKI unit signs the granter's leaf under its own subject"; ok = let s = baoGrantHere.systemd.services.swarm-bao-pki.script; in lib.hasInfix "/granter.pem ]" s && lib.hasInfix "bao-granter \"\" clientAuth" s; } { # The granter writes pki roles through `roles/swarm-*` only, so a role # named otherwise is refused at eval rather than 403'd at deploy. name = "a pki role name outside swarm-* is refused, naming both options"; ok = let names = a: lib.hasInfix "services.hyperhive.deploy.bao.servicesPkiRoleName" a.message && lib.hasInfix "services.hyperhive.deploy.bao.natsPkiRoleName" a.message; in lib.any (a: !a.assertion && names a) baoGranterOddPkiRole.assertions && !(lib.any (a: !a.assertion && names a) baoGrantHere.assertions); } { # 🩸 The refusal half of the forwarder's own leaf. It renders wherever the # store does and has no mode without a secret, so a null pair has one # fallback left — the hive's leaf and its union grant. Refused at eval, # with both options named. name = "a store host without the forwarder's own pair is refused, naming both options"; ok = let refused = lib.filter (a: !a.assertion) baoNoForwarderIdentity.assertions; names = a: lib.hasInfix "services.hyperhive.deploy.bao.forwarderOidcClientCertFile" a.message && lib.hasInfix "services.hyperhive.deploy.bao.forwarderOidcClientKeyFile" a.message; in lib.any names refused # The control: the same store with the pair in place trips no such # assertion, so the arm above is not firing on every store host. && !(lib.any (a: !a.assertion && names a) baoGrantHere.assertions); } { # The policy authorising this route lives in another file, and nothing # else relates the grants to the paths the code actually writes. # # `secret/data/` is KV v2's ACL prefix; `swarm` is # `swarm_secret_client::path::ROOT` and `agents` is # `Kind::Agent.as_str()`, both of which that crate pins in its own test. # # The grant is still the agent kind alone because nothing writes another # one yet. It widens when a path outside `agents/` gains a writer, not # when the kinds are declared. name = "the controller may write agent credentials, and only under the agent prefix"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; in lib.hasInfix "secret/data/swarm/agents/*" s && !(lib.hasInfix "secret/data/*" s) && !(lib.hasInfix "path \"secret/*\"" s); } { # The exact list is the property, not an accident of how it was typed. # `read` is in it because `mint_and_verify` reads a queue credential back # before rewriting it; `list` is not, so the controller can fetch a # credential only for an agent it was handed the name of, never enumerate # the tree. Pinned as the whole capability list, because an added # capability is exactly what a presence check misses. name = "the controller's grant on agent credentials is create/read/update and nothing else"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; in lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"read\", \"update\"]" s; } { # The swarm appservice token is a homeserver-admin credential. The # controller mints agents' accounts with it and has no business replacing # it: matrix-ctl is its one writer. Pinned as the whole stanza, so an # added capability fails. name = "the controller reads the swarm appservice token and cannot write it"; ok = lib.hasInfix "path \"secret/data/swarm/controller/swarm-controller/matrix/appservice-token\" {\n capabilities = [\"read\"]\n}" baoGrantHere.systemd.services.swarm-bao-controller-policy.script; } { # Every role lives under a mount nothing else creates, and the granter # holds no `sys/auth`, so the token-holding unit creates it — otherwise # every certificate login fails against a path that is not there. name = "the granter's role unit creates the cert auth mount, and the controller's unit writes its role"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; g = baoGrantHere.systemd.services.swarm-bao-granter-role.script; in lib.hasInfix "bao auth enable cert" g && !(lib.hasInfix "bao auth enable" s) && lib.hasInfix "auth/cert/certs/swarm-controller" s && lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s; } { # Same shape as the cert mount above, for the engine the controller # writes credentials through: a fresh store has no `secret/`, so the # grant would name a mount nobody created and the first write would 404. # # ⚠️ Matched on the COMMAND, for the reason the no-client-CA case below # spells out: the policy text is embedded in this same script and grants # `secret/data/...`, so any arm keyed on the *path* is satisfied either # way and could never fail. name = "the granting unit creates the KV mount the controller writes through"; ok = let s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script; in lib.hasInfix "bao secrets enable -path=secret kv-v2" s; } { # 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); } { # The operator writes this policy by hand, so a call the token-holding # unit makes and the file does not grant is a one-time step that fails. # Failing names every ungranted call. name = "every bao call the bootstrap-token unit makes is granted by bao-bootstrap-policy.hcl" + lib.optionalString (bootstrapUngranted != [ ]) ( ": " + lib.concatStringsSep "; " bootstrapUngranted ); ok = bootstrapUngranted == [ ]; } { # The same check for the granter: a grant a unit writes outside its # globs is a 403 on deploy. Failing names every ungranted call. name = "every bao call a granting unit makes is granted by the granter's policy" + lib.optionalString (granterUngranted != [ ]) (": " + lib.concatStringsSep "; " granterUngranted); ok = granterUngranted == [ ]; } { # What makes the case above mean something: discovery by the granter's # certificate reaches all ten units, and each yields calls. name = "the granter-policy check sees all ten granting units, and parses calls from each"; ok = lib.sort lib.lessThan (lib.attrNames granterUnits) == lib.sort lib.lessThan grantingUnitNames && lib.all (u: baoCalls u.script != [ ]) (lib.attrValues granterUnits) && lib.all (u: baoCalls u.script != [ ]) (lib.attrValues bootstrapUnits); } { # And the grants side: a stanza the parser skipped would read as a # grant that is not there. name = "every path stanza in bao-bootstrap-policy.hcl and the granter's policy parses"; ok = lib.all ( t: let grants = grantsIn t; in grants != [ ] && lib.length grants == lib.length (matches ''path "'' t) && lib.all (g: g.caps != [ ]) grants ) [ bootstrapPolicyText granterPolicyText ]; } { # The bootstrap policy, whole: the auth mounts and the granter's own two # objects, and nothing a `swarm-*` grant lives at. name = "the bootstrap policy is exactly the auth mounts and the granter's policy and role"; ok = lib.map (g: g.path) bootstrapGrants == [ "sys/auth" "sys/auth/cert" "sys/auth/approle" "sys/policies/acl/bao-granter" "auth/cert/certs/bao-granter" ] && grantFor bootstrapGrants "sys/policies/acl/swarm-controller" == null; } ]; in runGroup "bao-grants" cases