Watch
0
0
Fork
You've already forked hyperhive
0

hive-forge: the controller-token mint writes straight to the store, and the on-disk token goes

`forgejo-swarm-controller-account`, in the forge's container, now puts
the swarm-controller's forge token into the swarm secret store itself,
under the forge publisher's leaf. The container already receives that
leaf as nspawn system credentials for its journal forwarder; the mint
loads the same three. The token reaches bao on stdin (`value=-`) and
the forge's API through a header file, never as an argument, and never
touches disk.

The mint's guard is a marker holding the published token's name, at
/var/lib/forgejo/swarm-controller-token-name, written last: after the
put, and after every other `swarm-controller-boot-*` token of the
account is revoked through forgejo's admin API, authenticated as the
new token. So a put that failed, or a marker lost while the store kept
an older token, leaves no live token behind once a mint completes. The
mint logs in to the store and checks that the account is a site admin
before it mints, so a store that is down or an account the admin API
would refuse mints nothing on each retry. Failures retry under
store-retry; forgejo does not wait for the mint. No bao read and no
grant change: the forge publisher keeps create/update on the token.

Removed: the host's `swarm-bao-forge-publisher` service and path unit,
which read the token file through the container's root, and their
entries in the store and policy ordering glue. The forge publisher's
policy unit stays; `swarm-bao-forge-oidc` still uses the leaf.

On deploy, an existing forge host has no marker, so the mint runs once:
it mints a new token, publishes it, revokes the old one, and deletes
the old /var/lib/forgejo/swarm-controller-token. The controller's next
forge call gets a 401, re-reads the store and carries on.
This commit is contained in:
atlas 2026-10-16 14:39:19 +02:00 • committed by mara
commit 141be16c6d
14 changed files with 294 additions and 261 deletions

View file

@ -107,7 +107,7 @@ let
# ./grafana.nix's `grafanaRefusedFor`.
refusedOver = m: needle: lib.any (a: !a.assertion && lib.hasInfix needle a.message) m.assertions;
publisher = m: m.systemd.services.swarm-bao-forge-publisher;
mint = m: m.containers.hive-forge.config.systemd.services.forgejo-swarm-controller-account;
oidcFetch = m: m.systemd.services.swarm-bao-forge-oidc;
forgeSettings = forgeHere.containers.hive-forge.config.services.forgejo.settings;
@ -125,7 +125,6 @@ let
ok =
!bare.services.hyperhive.deploy.forgejo.enable
&& !(bare.containers ? hive-forge)
&& !(bare.systemd.services ? swarm-bao-forge-publisher)
&& !(lib.elem "forge.t.local" bare.services.hyperhive.gateway.localNames);
}
{
@ -327,55 +326,93 @@ let
&& refusedOver ciUnderForgeNoIdentity "services.hyperhive.deploy.bao.enable";
}
{
# The forge's host publishes the controller's token under its own leaf,
# which the store mints beside it here. `value=@` is what keeps the token
# out of the process's arguments.
name = "the forge's host publishes the controller's forge token to the store under its own leaf";
# The mint, in the forge's container, writes the controller's token to
# the store under the forge host's leaf, loaded as a credential. On
# stdin, so the token is no process's argument; the copy an earlier mint
# kept on the container's disk goes once a token is published, and no
# host unit reads into the container.
name = "the forge's mint writes the controller's forge token straight to the store and keeps no copy";
ok =
let
u = publisher allLocal;
u = mint allLocal;
cred = id: "/run/credentials/forgejo-swarm-controller-account.service/${id}";
in
u.environment.BAO_CLIENT_CERT == "/var/lib/swarm-bao-pki/forge-publisher.pem"
&& u.environment.BAO_CLIENT_KEY == "/var/lib/swarm-bao-pki/forge-publisher-key.pem"
&& lib.hasInfix "bao kv put secret/swarm/controller/swarm-controller/forge/token value=@\"$src\"" u.script
&& lib.hasInfix "/var/lib/nixos-containers/hive-forge/var/lib/forgejo/swarm-controller-token" u.script
&& lib.elem "container@hive-forge.service" u.wantedBy
# The deploy that adds the unit starts it only through a target.
&& lib.elem "multi-user.target" u.wantedBy
lib.elem "forge-bao-cert" u.serviceConfig.LoadCredential
&& lib.elem "forge-bao-key" u.serviceConfig.LoadCredential
&& u.environment.BAO_CLIENT_CERT == cred "forge-bao-cert"
&& u.environment.BAO_CLIENT_KEY == cred "forge-bao-key"
&& lib.elem "--load-credential=forge-bao-cert:/var/lib/swarm-bao-pki/forge-publisher.pem" allLocal.containers.hive-forge.extraFlags
&& lib.elem "--load-credential=forge-bao-cert:/etc/pki/forge-publisher.pem" forgeAlone.containers.hive-forge.extraFlags
&& lib.hasInfix "printf '%s' \"$token\" | bao kv put secret/swarm/controller/swarm-controller/forge/token value=-" u.script
&& !(lib.hasInfix "value=@" u.script)
&& !(lib.hasInfix "atomic_write_secret" u.script)
&& lib.hasInfix "rm -f /var/lib/forgejo/swarm-controller-token\n" u.script
&& !(allLocal.systemd.services ? swarm-bao-forge-publisher)
&& !(allLocal.systemd.paths ? swarm-bao-forge-publisher)
&& !(forgeAlone.systemd.services ? swarm-bao-forge-publisher)
&& !(allLocal.systemd.services ? hive-forge-swarm-controller-token);
}
{
# What makes it re-run. A oneshot left `active (exited)` makes every later
# start a no-op, so `RemainAfterExit` would silence every trigger: the
# targets and the container's start (`wantedBy`, above) and a write of
# the token file. A missing file exits 0, so a switch with the container
# down does not fail.
name = "the forge publisher re-runs on each container start and each write of the token file";
# The order that keeps a token live only if the store has it: log in
# before the mint, put, revoke every other `swarm-controller-boot-*`
# token, and only then write the marker, which holds the name. A marker
# present skips all of it.
name = "the forge's mint writes its marker only after the put and the revocation of every earlier token";
ok =
let
p = allLocal.systemd.paths.swarm-bao-forge-publisher;
s = (mint allLocal).script;
at = needle: lib.stringLength (lib.head (lib.splitString needle s));
order = l: lib.length l < 2 || (at (lib.elemAt l 0) < at (lib.elemAt l 1) && order (lib.tail l));
steps = [
"if [ -s \"$name_path\" ]; then"
"forgejo admin user list --admin"
"bao login -method=cert"
"forgejo admin user generate-access-token"
"bao kv put"
"forge GET \"/admin/users/swarm-controller/tokens?limit=50&page=$page\""
"forge DELETE \"/admin/users/swarm-controller/tokens/$n\""
"rm -f /var/lib/forgejo/swarm-controller-token\n"
"mv -f \"$name_path.tmp\" \"$name_path\""
];
in
!((publisher allLocal).serviceConfig.RemainAfterExit or false)
&&
p.pathConfig.PathChanged
== "/var/lib/nixos-containers/hive-forge/var/lib/forgejo/swarm-controller-token"
&& p.pathConfig.Unit == "swarm-bao-forge-publisher.service"
&& lib.hasInfix "the path unit publishes it when it does" (publisher allLocal).script
&& lib.hasInfix "exit 0" (publisher allLocal).script
&& lib.elem "multi-user.target" p.wantedBy
&& !(bare.systemd.paths ? swarm-bao-forge-publisher);
lib.all (step: lib.hasInfix step s) steps
&& order steps
&& lib.hasInfix "name_path=/var/lib/forgejo/swarm-controller-token-name" s
&& lib.hasInfix "printf '%s\\n' \"$name\" >\"$name_path.tmp\"" s
&& lib.hasInfix "swarm-controller-boot | swarm-controller-boot-*) stale+=(\"$n\") ;;" s
&& lib.hasInfix "-H @<(printf 'Authorization: token %s\\n' \"$token\")" s;
}
{
# A store that is down retries the mint (../host-modules/lib/store-retry.nix),
# and forgejo never waits for it.
name = "the forge's mint retries on failure, and forgejo does not wait for it";
ok =
let
c = allLocal.containers.hive-forge.config.systemd.services;
u = mint allLocal;
in
u.serviceConfig.Restart == "on-failure"
&& u.startLimitBurst == 2880
&& u.serviceConfig.RemainAfterExit
&& lib.elem "forgejo.service" u.after
&& !(lib.elem "forgejo-swarm-controller-account.service" (
c.forgejo.after ++ c.forgejo.requires ++ c.forgejo.wants
))
&& lib.elem "forgejo-swarm-controller-account.service" c.swarm-bridge-dns.before;
}
{
# The absence arm: a forge host without the store has no leaf unless one
# is placed, and the unit says which options to set.
name = "a forge host with no publisher leaf renders a publisher that fails naming the options";
# is placed, and the mint says which options to set before it mints.
name = "a forge host with no publisher leaf renders a mint that fails naming the options, minting nothing";
ok =
let
s = (publisher forgeHere).script;
u = mint forgeHere;
in
lib.hasInfix "forgePublisherClientCertFile" s
&& lib.hasInfix "exit 1" s
&& !(lib.hasInfix "bao kv put" s);
!(u.environment ? BAO_CLIENT_CERT)
&& lib.hasInfix "forgePublisherClientCertFile" u.script
&& lib.hasInfix "exit 1" u.script
&& !(lib.hasInfix "forgejo admin user generate-access-token" u.script)
&& !(lib.hasInfix "bao kv put" u.script);
}
{
# The forge's OIDC secret has one route, the store, under the forge