nix: split module-eval into per-subsystem checks
The single module-eval derivation forced ~62 full nixosSystem fixtures live at once to compute its cases list: 10.6GB peak RSS / 5m25s to evaluate, by far the dominant cost in nix flake check. Splits it into 21 independent checks.module-eval-* derivations (1-7 fixtures each) sharing builders/helpers via module-eval/lib.nix, so no single derivation needs more than a handful of fixtures live at once. A few cases spanning two clusters carry a small duplicated fixture rather than threading shared state through lib.nix.
This commit is contained in:
parent
69b70a9c6f
commit
dc418a5223
24 changed files with 3760 additions and 2997 deletions
236
nix/module-eval/bao-grants.nix
Normal file
236
nix/module-eval/bao-grants.nix
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# `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
|
||||
# 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";
|
||||
};
|
||||
|
||||
# The store and the token, with no CA to trust. `mkForce` because the PKI
|
||||
# glue supplies one by default here — this is the deployment that brings its
|
||||
# own certificates and has not named the authority yet, and it separates
|
||||
# "the grant unit runs" from "cert auth can be set up".
|
||||
baoGrantNoClientCa = hive {
|
||||
deploy.bao.enable = true;
|
||||
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
|
||||
deploy.bao.clientCaFile = lib.mkForce null;
|
||||
};
|
||||
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 with a placed bootstrap token renders the granting unit on the host";
|
||||
ok =
|
||||
let
|
||||
u = baoGrantHere.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 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 with a placed bootstrap token renders the publisher's granting unit too";
|
||||
ok =
|
||||
let
|
||||
u = baoGrantHere.systemd.services.swarm-bao-secret-publisher-policy;
|
||||
in
|
||||
u.unitConfig.ConditionPathExists == "/run/secrets/bao-bootstrap.token"
|
||||
&& 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 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);
|
||||
}
|
||||
{
|
||||
# Write-only is the property, not an accident of how it was typed: a
|
||||
# `read` here would let the controller recover every agent's credentials
|
||||
# instead of only replacing them. 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 write-only";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix "path \"secret/data/swarm/agents/*\" {\n capabilities = [\"create\", \"update\"]" s;
|
||||
}
|
||||
{
|
||||
# The policy above grants paths under a mount nothing else creates, so
|
||||
# the unit that writes the policy has to create it too — otherwise every
|
||||
# certificate login fails against a path that is not there.
|
||||
name = "the granting unit creates the cert auth mount and the controller's role";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantHere.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix "bao auth enable cert" s
|
||||
&& lib.hasInfix "auth/cert/certs/swarm-controller" s
|
||||
&& lib.hasInfix "/var/lib/swarm-bao-tls/client-ca.pem" s;
|
||||
}
|
||||
{
|
||||
# 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;
|
||||
}
|
||||
{
|
||||
# The arm that makes the one above mean something. A role's trust anchor
|
||||
# is the CA, so with none named there is nothing to write — and the
|
||||
# policy write, which needs no CA, must survive that.
|
||||
#
|
||||
# ⚠️ Matched on the COMMANDS, not on `auth/cert/certs`: the policy text is
|
||||
# embedded in this same script and grants that very path, so the shorter
|
||||
# infix is present either way and the arm could never fail.
|
||||
name = "with no client CA the unit still writes the policy and skips the role";
|
||||
ok =
|
||||
let
|
||||
s = baoGrantNoClientCa.systemd.services.swarm-bao-controller-policy.script;
|
||||
in
|
||||
lib.hasInfix "bao policy write" s
|
||||
&& !(lib.hasInfix "bao auth enable cert" s)
|
||||
&& !(lib.hasInfix "client-ca.pem" s)
|
||||
# The KV mount is NOT part of what a missing client CA switches off:
|
||||
# the controller writes through it whether or not anything can log in
|
||||
# by certificate. Asserted here rather than trusted, because both
|
||||
# steps live in the same script and one indentation level decides it.
|
||||
&& 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);
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "bao-grants" cases
|
||||
Loading…
Reference in a new issue