Swarm SSO
The swarm runs one authelia, and it is two things at once: the session
provider every protected vhost checks (auth_request), and — once any
client is declared — an OIDC provider issuing tokens to relying
parties: the forge and the matrix homeserver.
The second role is derived rather than switched:
services.hyperhive.swarm.authelia.oidc.clients being non-empty turns it
on. authelia refuses to start with a provider that has no clients, so a
separate enable would be a second fact free to disagree with the first.
Getting in the first time
authelia binds loopback only. The gateway on the host running it
publishes it as auth.<swarm.domain> — vhost, dnsmasq record and TLS
name all follow swarm.authelia.enable, so there is nothing to turn on
separately. (Details, including why a client hive must not declare that
vhost: ../gateway.md.)
Authelia does not start until at least one user exists. The user store is generated empty — deliberately, since seeding a default account would put a credential in a config file — but authelia validates it at startup and treats "no users" as fatal:
error reading the authentication database: could not validate the schema:
users: non zero value required
It then exits 1 and systemd restarts it, so a swarm that has been
enabled but not bootstrapped shows a crash-looping unit and 502 Bad Gateway from the vhost — not a login page with nobody able to use it.
The gateway is working in that state; the upstream is not up.
⚠️ So the step below is required to finish the install, not an optional first-login convenience. Run it before concluding anything is wrong with the proxy: a 502 here means "no users yet" far more often than it means a routing fault.
Add the first subject on the host running authelia:
# swarmctl user add mara --display-name Mara --email mara@example.com --group admins
added mara to /var/lib/authelia-swarm/users.yml
password: <generated>
this password is stored nowhere — record it now
The password is generated, hashed, and printed once; only the hash is
kept. swarmctl reads and writes authelia's users.yml directly — it is
the one user store, shared with swarm-authelia-bridge, which creates
agent identities in the same file. No restart: authelia watches it. Full
reference: ../tools/swarmctl-cli.md.
You can edit users.yml by hand, and swarmctl will read what you
wrote. ⚠️ It rewrites the whole file on every change, so comments and
formatting do not survive; values and unrecognised keys do.
This step stays manual on purpose. Bootstrapping an identity provider non-interactively means a secret arriving from somewhere — a file, an env var, a nix expression — and every one of those is worse than an operator typing one command once.
Changing a subject afterwards
user add only ever adds: on a name that already exists it refuses,
rather than resurfacing as a second account or a silent overwrite.
Editing an existing subject is user update, and the flags compose, so
one call can change several things:
# swarmctl user update mara --add-group admins --email mara@example.com
added to group "admins"
email: unset -> "mara@example.com"
mara is now in groups: admins
Two behaviours worth knowing before you rely on them:
--remove-groupfails if the user is not in that group. Every other flag is idempotent — setting what is already set is fine, so a "make these four things true" call does not break when one of them already was. Revocation is the exception on purpose: a typo'd group name that reported success would leave an account holding access you believe you took away, and that is the one outcome nobody re-checks.- The resulting group list is printed because group names have no
registry anywhere. A misspelled
--add-groupcreates a real group that no access-control rule mentions, so the user gains nothing and no error is possible — reading the line back is the only check there is.
Passwords are deliberately out of scope here: regenerating a credential is a different intent from editing an attribute, and folding them means an attribute edit can invalidate a login by accident.
What secrets exist, and where each one lives
Every secret in the swarm, with its generator and its path, is tabulated
in one place: secrets.md, including authelia's own keys
(session, JWT, storage-encryption, OIDC HMAC, OIDC issuer) and the two
halves of each client secret. That page's two rules — a secret is always
a path, never a value, and the generator and the reader are usually in
different containers — are why the client secret's plaintext half needs
the delivery step below and the rest of authelia's keys don't.
Getting the plaintext to the relying party
Three cases, and they are genuinely different mechanisms rather than one mechanism with flags.
1. All-local — one host runs both
Nothing to configure beyond swarm.forge.sso.enable = true or
swarm.matrix.sso.enable = true. Per service, a host-side unit waits for
authelia's first boot to mint that client's secret and copies it into the
service's container, and the service's own module contributes its client
entry — callback URL included — to authelia's client list.
The callback is built once and read twice, so the redirect URI authelia is told to allow and the one the service actually sends cannot drift apart. A mismatch there is a rejected login with no error text worth reading.
⚠️ The delivery is a copy, not a bindMounts entry, and deliberately so:
nixos-container refuses to start a container whose bind source is
missing, and this secret does not exist until authelia's first boot has
run. Binding it would make the service wait on a file that waits on a
container that starts after it — on a fresh hive, a permanent stall
presenting as "the forge is broken", several layers from its cause.
2. Swarm-managed services
The controller side owns provisioning: swarmctl writes both halves, the
same way it already owns authelia's user store (users.yml, read and
written in place).
3. A hive elsewhere
No shared host, so no automatic path. The operator provides the file and names it:
services.hyperhive.swarm = {
authelia.url = "https://auth.example.com";
forge.sso = {
enable = true;
clientSecretFile = "/var/lib/hyperhive/forge-oidc-secret";
};
};
Both are asserted at eval. A hive that boots with SSO half-configured shows a login button that always fails — a symptom several layers from its cause, and far worse to diagnose than an evaluation error.
Where each relying party differs
The registration half is identical; what each service does with the result is not.
| forge | matrix | |
|---|---|---|
| how it learns the config | a oneshot calls forgejo admin auth, writing a login-source row into its database |
tuwunel reads a [[global.identity_provider]] entry from its config file |
| how it reads the secret | a path inside its container | the same path, handed on by LoadCredential |
| callback URL | <root>/user/oauth2/<source>/callback |
<homeserver>/_matrix/client/unstable/login/sso/callback/<client_id>, a shape tuwunel fixes rather than accepts |
| cost of a malformed entry | the login source is missing | the homeserver can refuse to start |
Two consequences worth stating plainly:
- tuwunel re-reads its secret file on every OAuth exchange, not only
at startup, and its own sandboxing hides most paths from it. It gets the
file through
LoadCredentialfor the same reason the registration token does — that keepsDynamicUserandPrivateUsersintact, with no host-side ownership arrangement to maintain. - Matrix SSO lives inside the homeserver. The client-server API is
spoken by non-browser clients holding matrix access tokens — every
agent's own daemon — as well as by federation, so
/_matrix/is served directly and authenticates itself. The forward-auth vhosts protect browser surfaces; this is not one of them.
What this does not do
- It does not disable local login. Each service keeps its password
database and gains a second door. An identity provider that can take a
service offline when it hiccups is worse than one with two ways in.
Making authelia the only path is a separate, reversible switch per
service (tuwunel's
login_with_password, forgejo's own setting). - It does not provision users. Agents are created and destroyed
continuously, so the subject set belongs to a program rather than to a
config file; today that program is
swarmctl.