hyperhive/nix/module-eval/bao-grants.nix
atlas 5cd7f866f4 swarm-bao: grant the agent PKI mount (for #4756)
#4756 moves agent client certificates onto a PKI mount of their own,
`pki-agents`, whose root bao generates internally. The unit that sets
that mount up runs as the bao granter, and the granter's policy is only
written while #4754's one-time bootstrap token is in place. Adding these
grants after an operator has done that step would cost a second token
placement, so they go into the granter's policy here, before it.

Six stanzas: enable and tune the mount, list its issuers, read its CA,
generate its root internally, and write `swarm-*` roles on it. No root
delete or sudo: agent cert-auth roles pin that root by value, so
replacing it must not be something a deploy can do.

Adds deploy.bao.agentPkiMountPath (default `pki-agents`), which the
stanzas are rendered from. The module-eval case pinning the granter's
policy now lists all seventeen stanzas, and the "grants nothing outside"
case also refuses the agent mount's root, issue, sign and a roles/*
glob.
2026-09-27 22:57:46 +02:00

1168 lines
50 KiB
Nix

# `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 <cmd> -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/<hive>/*` 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