hyperhive/swarm-authelia-bridge/README.md
atlas 3700167279 feat(swarm-authelia-bridge): mark agent identities with a group, and answer for the set
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.
2026-08-20 00:15:48 +02:00

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's BridgeRequest verbatim — EnsureAgentIdentity (idempotently ensure an agent exists as an authelia subject) and ListAgentIdentities (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-located swarm-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.