bao: write the swarm controller's policy from inside the store
Cert auth answers a role, so nothing in the swarm can authenticate to the store until some role exists. Creating the first one therefore cannot itself use a certificate — the credential has to come from outside that cycle, and an operator places it. `deploy.bao.bootstrapTokenFile` names that token. A one-shot unit inside the store's container reads it and writes the `swarm-controller` policy; `local-defaults.nix` supplies the path (never the file) on an all-local deploy, since co-location makes only the location derivable. `bao operator init` stays an operator action in every shape. The unit runs in the container rather than on the host because writing the first grant is a store-side operation: it reaches the store locally and needs no client certificate. Gating it on the store being here is therefore not the co-location assumption glue-matrix-bao-token.nix warns about — a reader has to work from anywhere, the first write never does. Policy only, deliberately: a cert-auth role binds a certificate and the controller has no bao identity yet — it holds no leaf and contains no bao code at all. The two certificates that do exist are both wrong to bind. `clientCertFile` is the store host's own reader leaf rather than the controller's, and the CA that signed it also signs every other reader's, so binding that would let any reader authenticate as the controller. Whoever gives the controller an identity writes the role. The grants are scoped to `hive-*`. Without the prefix the controller could rewrite the policy that constrains it. Two things the module-eval arms pin: the unit renders inside the container with the token path in both its script and its ConditionPathExists, and a host that names a token while running no store gets neither the unit nor the host-side directory. The capabilities were derived with `bao write -output-policy` rather than written from memory, and the setup.md commands were run against a real binary for shape before being written down. Refs #3726.
This commit is contained in:
parent
6fd5bc1c4f
commit
2f7d3e02e9
4 changed files with 223 additions and 0 deletions
|
|
@ -203,6 +203,20 @@ let
|
|||
# is deployed" from "this host can authenticate to the store".
|
||||
matrixNoBaoIdentity = hive { deploy.matrix.enable = true; };
|
||||
|
||||
# The store, plus a placed bootstrap token: the only shape in which the
|
||||
# swarm's first grant 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";
|
||||
};
|
||||
|
||||
baoNames = machine: machine.services.hyperhive.gateway.localNames;
|
||||
|
||||
baoTwoAddresses = hive {
|
||||
|
|
@ -428,6 +442,39 @@ let
|
|||
lib.any (c: lib.hasInfix "/var/lib/swarm-otel-oidc/by-hand.secret" c)
|
||||
otelRemoteAuthelia.containers.swarm-otel.config.systemd.services.opentelemetry-collector.serviceConfig.LoadCredential;
|
||||
}
|
||||
{
|
||||
# Reads the rendered unit INSIDE the container, which is where the write
|
||||
# happens: reaching the store locally is what lets the grant be written
|
||||
# without a client certificate at all.
|
||||
name = "a store host with a placed bootstrap token renders the granting unit inside the container";
|
||||
ok =
|
||||
let
|
||||
u = baoGrantHere.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy;
|
||||
in
|
||||
lib.hasInfix "/run/secrets/bao-bootstrap.token" u.script
|
||||
&& u.unitConfig.ConditionPathExists == "/run/secrets/bao-bootstrap.token";
|
||||
}
|
||||
{
|
||||
# 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.containers.swarm-bao.config.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix "sys/policies/acl/hive-*" s && !(lib.hasInfix "sys/policies/acl/*" 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.
|
||||
name = "a bootstrap token on a host that runs no store grants nothing";
|
||||
ok = !(baoGrantNoStore.systemd.services ? swarm-bao-bootstrap-dir);
|
||||
}
|
||||
{
|
||||
name = "a config written against the pre-rename authelia usersFile still reaches the bridge";
|
||||
ok =
|
||||
|
|
|
|||
Loading…
Reference in a new issue