# 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 like the forge. 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.` — 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`](../gateway.md).) Reachable is not the same as usable: the provider is generated with an empty user set, deliberately. A provider with nobody in it yet is the correct state for a fresh swarm — it is not a half-finished install, and seeding a default account would be a credential in a config file. Add the first subject on the host running authelia: ```console # swarmctl user add mara --display-name Mara --email mara@example.com --group admins added mara to /var/lib/authelia-swarm/users.yml password: this password is stored nowhere — record it now ``` The password is generated, hashed, and printed once; only the hash is kept. `swarmctl` writes its canonical `users.json`, re-renders authelia's `users.yml` from it, and restarts authelia. Full reference: [`../tools/swarmctl-cli.md`](../tools/swarmctl-cli.md). 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. ## What secrets exist, and where each one lives | secret | generated by | rests in | read by | |---|---|---|---| | `jwt.key`, `session.key`, `storage-encryption.key` | authelia's first-boot unit | `/var/lib/authelia-swarm/` | authelia | | `oidc-hmac.key` | same unit | same directory | authelia | | `oidc-issuer.key` (RSA) | same unit | same directory | authelia signs with it; clients verify the **public** half at `/jwks.json` | | `oidc-clients/.digest` | same unit, via `authelia crypto hash generate` | same directory, merged in through `settingsFiles` | authelia | | `oidc-clients/.secret` | the same mint — this is its plaintext half | same directory | **the relying party, in another container** | Everything above the last row is generated in-container because nothing outside that container ever reads it. That is the test worth applying to any secret added here. The last row fails it, and that is the entire reason a delivery step exists. **None of it is ever written into a nix expression.** authelia's `settings` are rendered into the nix store, which is world-readable and permanent, so the client digest reaches authelia through `settingsFiles` (merged at runtime) and every other secret through a `*File` option carrying a path rather than a value. ## 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`. A host-side unit waits for authelia's first boot to mint the secret and copies it into the forge container, and the forge module contributes its own client entry — callback URL included — to authelia's client list. The callback is built from the same source name the registration uses, so the redirect URI authelia is told to allow and the one forgejo 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 forge 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.json` canonical, `users.yml` a rendered artifact). ### 3. A hive elsewhere No shared host, so no automatic path. The operator provides the file and names it: ```nix 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. ## What this does not do - **It does not disable local login.** The forge keeps its password database and gains a second door. An identity provider that can take the forge offline when it hiccups is a worse forge than one with two ways in. - **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`.