The users database holds humans and agents in one namespace and nothing in it said which was which, so a roster read had no predicate to read with. Marks positively, at creation. The alternative — everyone who is not an operator — fails in the direction that matters: an account created without a group is an operator who cannot log in, a mistake the swarm UI docs already warn about, and it would have rendered as an agent. The group is a constant for the same reason the operator group it mirrors is one. An identity that predates the marker gains it when agent creation runs again, which is already the agreed migration for those; AlreadyExists therefore reports that the subject was there, not that nothing was written. ListAgentIdentities reads through this process because the store is owned by a uid swarm-controller does not have — the same reason the write goes through here — and answers with names alone, never the digests it sits next to.
2.9 KiB
swarm-authelia-bridge
The only thing allowed to write swarm-authelia's users database.
Why this exists
swarm-controller's CreateIdentity job needs to add an agent as an
authelia subject, but swarm-controller runs unprivileged and does not own
users.yml — a different uid does (authelia-swarm, the user
services.authelia.instances.swarm runs as). Root/CAP_CHOWN/a shared
group were all examined and rejected during this crate's design thread —
see swarmctl/README.md's identical analysis of this same file, written
before this crate existed.
The fix: run this process as User = "authelia-swarm"; instead —
inside the swarm-authelia container, alongside authelia itself — so it
simply owns the file it writes. No elevated privilege anywhere.
Shape
- One endpoint,
POST /requests, body =swarm-authelia-bridge-sock'sBridgeRequestverbatim —EnsureAgentIdentity(idempotently ensure an agent exists as an authelia subject) andListAgentIdentities(the roster: the subjects carrying the agent marker group, names only). - Bearer-authenticated via authelia's own OIDC token introspection (RFC
7662), against
swarm-controller's existing machine-client identity (already minted for the queue connection) — no new credential. - No restart of authelia after a write — relies on
authentication_backend.file.watch, confirmed working against the pinned 4.39.20 build during this design work. - Network-facing rather than unix-socket-only:
swarm-authelia's container shares the host netns, so the same listener serves both a co-locatedswarm-controller(loopback) and a split-host one (bind wider + firewall) with no separate transport.
One store, two writers
This bridge and swarmctl both read and write authelia's users.yml
directly. That is the whole arrangement — neither keeps a private copy it
considers canonical.
It used to be otherwise, and the seam was real: each side had its own
users.json treated as authoritative, rendering the same physical
users.yml. A writer whose own JSON was missing could not tell "nothing
here yet" from "someone else's users", so it refused to write at all —
which is exactly what a hive with existing users hit (#3422).
What still has to hold, since two processes share the file:
- Read before write. Both do, so neither can drop a user the other added between operations.
- Unknown keys survive. Both carry unmodelled top-level and per-user fields through a round-trip, or whichever writes second would silently delete what the first added.
- Every user gets an email. A relying party asking for the claim fails rather than degrades, so both write paths fill in a synthetic address when none was supplied.
Neither restarts authelia: it watches the file. swarmctl could (it is
root); this bridge deliberately cannot, and a reload that depends on which
process wrote is not a reload.