Watch
0
0
Fork
You've already forked hyperhive
0

swarm: OIDC clients registered wherever authelia runs; nats + matrix secrets from bao

The three client registrations move into glue-{controller,nats,matrix}-oidc-client.nix,
gated only on deploy.authelia.enable and reading their ids from swarm.* options,
so the host running authelia registers every one of them whichever host runs
the service. The matrix callback is a new read-only swarm.matrix.sso.redirectUri,
read by both the homeserver's config and its client entry.

swarm-secret-publish now also publishes the queue's and the homeserver's minted
secrets to swarm/services/<id>/oidc/client. The queue host reads its responder's
secret there with swarm-bao-nats-auth-oidc under the responder's own leaf
(swarm-nats-auth role gains that one read path), and the homeserver's host with
swarm-bao-matrix-oidc under matrix-ctl's leaf (swarm-matrix-ctl role gains its
one read path). The host-file copies are gone: swarm-nats-auth-secrets now
delivers only the callout seeds and the store identity, and
hive-matrix-oidc-secret is removed. A fetch unit whose leaf is unset still
renders and fails naming the option pair.

Closes #4943
This commit is contained in:
atlas 2026-10-05 02:56:18 +02:00 • committed by mara
commit 962da3b784
20 changed files with 702 additions and 293 deletions

View file

@ -98,6 +98,14 @@ let
deploy.swarm-otel.enable = true;
};
# The store and the homeserver on one host: the fetch of the homeserver's
# OIDC secret logs in under matrix-ctl's leaf, whose role this host writes.
baoGrantWithHomeserver = hive {
deploy.bao.enable = true;
deploy.bao.bootstrapTokenFile = "/run/secrets/bao-bootstrap.token";
deploy.matrix.enable = true;
};
# The store with none of the readers beside it. Grafana and the collector 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.
@ -426,23 +434,26 @@ let
);
}
{
# 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.
# The third principal's grant: two paths, each 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.
#
# The leaf is the swarm appservice token, which matrix-ctl mints and
# publishes for the controller. Counted, so a second stanza fails rather
# than riding along beside a correct one.
name = "matrix-ctl's grant is the swarm appservice token, nothing else";
# The first leaf is the swarm appservice token, which matrix-ctl mints
# and publishes for the controller; the second is the homeserver's own
# OIDC client secret, read-only. Counted, so a third stanza fails rather
# than riding along beside correct ones.
name = "matrix-ctl's grant is the swarm appservice token and the homeserver's OIDC secret, nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-matrix-ctl-policy.script;
in
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) == 2
&& !(lib.hasInfix "secret/data/swarm/services" s)
&& lib.hasInfix "path \"secret/data/swarm/services/tuwunel/oidc/client\" {\n capabilities = [\"read\"]\n}" s
&& lib.length (lib.splitString "path \"" s) == 3
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
&& !(lib.hasInfix "swarm-grafana" s)
&& !(lib.hasInfix "secret/data/swarm/agents" s)
&& !(lib.hasInfix "secret/data/swarm/hives" s)
&& !(lib.hasInfix "sys/policies/acl" s);
@ -559,18 +570,20 @@ let
{
# `+` is one path segment, so this reaches `swarm/agents/<agent>/queue`
# and no other credential an agent holds; `swarm/agents/*` would reach
# all of them.
name = "the queue responder's grant is every agent's queue credential and nothing else";
# all of them. The responder's own OIDC client secret is the one
# `services/` leaf beside it.
name = "the queue responder's grant is every agent's queue credential and its own OIDC secret, nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-nats-auth-policy.script;
in
lib.hasInfix "path \"secret/data/swarm/agents/+/queue\" {" s
&& lib.hasInfix "capabilities = [\"read\"]" s
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 1
&& lib.hasInfix "path \"secret/data/swarm/services/swarm-nats/oidc/client\" {" s
&& lib.length (lib.filter lib.isList (builtins.split "capabilities = \\[\"read\"]" s)) == 2
&& lib.length (lib.filter lib.isList (builtins.split "path \"" s)) == 2
&& !(lib.hasInfix "secret/data/swarm/agents/*" s)
&& !(lib.hasInfix "secret/data/swarm/hives" s)
&& !(lib.hasInfix "secret/data/swarm/services" s)
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
&& lib.hasInfix "auth/cert/certs/swarm-nats-auth" s
&& lib.hasInfix "allowed_common_names=swarm-nats-auth" s
&& lib.hasInfix "token_policies=swarm-nats-auth" s;
@ -737,6 +750,23 @@ let
in
lib.all waitsFor (policyReaders ++ [ "swarm-bao-forwarder-oidc" ]);
}
{
# The homeserver's OIDC secret is read under matrix-ctl's leaf, so its
# reader waits for the unit writing matrix-ctl's role, not one named
# after itself. The second arm is the absence control: on the store's
# host without a homeserver or queue, the ordering defines no reader.
name = "the homeserver's OIDC secret reader is ordered after matrix-ctl's policy unit";
ok =
let
u = baoGrantWithHomeserver.systemd.services.swarm-bao-matrix-oidc;
noReaders = baoGrantNoReaders.systemd.services;
in
lib.elem "swarm-bao-matrix-ctl-policy.service" u.after
&& lib.elem "swarm-bao-matrix-ctl-policy.service" u.wants
&& !(lib.elem "swarm-bao-matrix-ctl-policy.service" u.requires)
&& !(noReaders ? swarm-bao-matrix-oidc)
&& !(noReaders ? swarm-bao-nats-auth-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