- docker-compose: CODER_OIDC_* wired to Authelia (password login kept as break-glass) - scripts/backup-stacks.sh + stacks-backup.timer: nightly pg_dump, Continue state, stack configs - vault: verification items 13/14, auth-model OIDC section, limits DNS-01 runbook
8.8 KiB
title, description, source, reviewed
| title | description | source | reviewed |
|---|---|---|---|
| 🔐 Auth model | Why code.loca.zone is deliberately not behind Authelia, why a browser-only gate is impossible, why the cookies do not collide, and how the initial Owner was bootstrapped over loopback. | verified deployment on the loca operator host, 2026-08-19; first-user bootstrap verified 2026-08-20 | 2026-08-20 |
🔐 Auth model
🧾 Decision
- Coder is the identity provider for its own deployment; the
code.loca.zonevhost has noauth_requestgate and no Authelia access rule - this is the only loca.zone application host that is intentionally ungated at the proxy, and the reason is protocol, not laziness
- Authelia was not modified for this stack: its
default_policyisbypass, so a host with no rule is simply passed through. Adding nothing was the correct action
💥 Precise failure mode if an Authelia rule were added
- the site-wide gate answers any request without a valid
authelia_sessioncookie with a302tohttps://auth.loca.zone - what that breaks, concretely:
- the
coderCLI, which authenticates with theCoder-Session-Tokenheader and has no cookie jar;coder login,coder templates push,coder sshandcoder port-forwardall receive a redirect where they expect JSON - workspace agents, which dial
CODER_ACCESS_URLand hold long-lived/api/v2websockets; a redirect on the upgrade request means the agent never connects and the workspace sits in a connecting state forever - relayed workspace traffic, since the built-in DERP relay rides the same vhost
- the
- upstream is explicit that all clients and agents must be able to reach the server over HTTP or HTTPS and that any proxy in between must support websockets; an unconditional redirect violates both
🚫 Why a partial browser-only gate is impossible
- the dashboard is a single-page application served from the same origin as the API; the browser and the agents both call
/api/v2/... - there is no stable path prefix that separates human traffic from machine traffic, and no header an nginx gate could trust to tell them apart, since a browser can be made to send anything a header-based rule would key on
- gating
/while exempting/api/v2would exempt the entire attack surface and leave only the static bundle protected, which is security theatre - the workspace app and web terminal routes (
/@user/workspace.agent/apps/...,/@user/workspace.agent/terminal) are proxied by the same server and are used by both planes - conclusion: either the whole vhost is gated, which breaks the product, or none of it is, which is what is deployed
🍪 Why the cookies do not collide
- Authelia's session cookie is domain-scoped to
loca.zone, soauthelia_sessionis presented on requests tocode.loca.zoneas well - Coder ignores it; it reads only its own
coder_session_token, which Coder sets host-scoped forcode.loca.zone - nothing writes both names, so no overwrite, no truncation and no cross-application session confusion
- practical consequence: being logged into
auth.loca.zonegrants nothing on this host, and being logged into Coder grants nothing on the Authelia-gated hosts
👤 First-user bootstrap is done
- state now, verified 2026-08-20: the initial Owner exists, so nothing here is waiting on a setup form
- username
loca, emailadmin@loca.zone, role Owner, status active - organization
coder, the default one, id9c28ebd1-4fe6-446b-83e2-f723954fec10 - created with
POST /api/v2/users/firstagainsthttp://127.0.0.1:52226, so the credential never traversed the public vhost GET /api/v2/users/firstflipped from404to200at that moment, which is upstream's own existence check for the initial user
- username
- how the password was handled, which is the only acceptable shape for an agent-run bootstrap:
- randomly generated, 32 characters, written straight into
/home/loca/dev/coder/.admin-credentialsat mode 600 in a git-ignored path - never displayed on a terminal, in a log, in a ticket, in a chat message or in this wiki, and never read back into any document
- that file also holds the session token minted by the same call, which is the second reason it stays at 600 and out of git
- randomly generated, 32 characters, written straight into
- rotation is the operator's responsibility and nobody else's: sign in at https://code.loca.zone and set a new password under Account then Security, after which the stored copy is stale by design
- what was verified alongside the bootstrap, none of it credential-bearing:
curl -sSI https://code.loca.zone/returns200with no auth redirectcurl -sf http://127.0.0.1:52226/api/v2/buildinforeportsv2.35.3+65e2bfbwithdashboard_urlhttps://code.loca.zone
🔒 No open signup
POST /api/v2/users/firstis the only account-creating endpoint reachable without authentication and it exists solely to create the initial OwnerPOST /api/v2/usersrequires authentication, so every later account is created by an admin, either withcoder users createor through Users then New user in the dashboard- there is no self-service registration in Coder OSS with password auth; a user appears only because an admin made it
- password reset for the initial Owner is a host-side operation:
coder reset-password <username>only works when run on the machine running the control plane, which here means inside thecodercontainer
🔗 Authelia as OIDC provider — configured
Implemented 2026-08-20. Identity is unified at the application layer: Coder consumes Authelia over OIDC. The proxy is still ungated — everything in the sections above still holds, and adding an auth_request gate would still break agents.
Authelia side, /home/loca/dev/services/authelia/config/configuration.yml, second entry under identity_providers.oidc.clients:
client_id: 'coder',client_name: 'Coder',public: falseclient_secretis apbkdf2-sha512digest read via{{ secret "/config/secrets/oidc/coder-client-secret.digest" }}; that file is600 root:root, and the container runs as root so the strict mode is safeauthorization_policy: 'one_factor',consent_mode: 'implicit',token_endpoint_auth_method: 'client_secret_basic'- single redirect URI
https://code.loca.zone/api/v2/users/oidc/callback - scopes
openid,profile,email,groups; grantauthorization_code; response typecode
Coder side, environment: on the coder service in docker-compose.yml:
| variable | value |
|---|---|
CODER_OIDC_ISSUER_URL |
https://auth.loca.zone |
CODER_OIDC_CLIENT_ID |
coder |
CODER_OIDC_CLIENT_SECRET |
${CODER_OIDC_CLIENT_SECRET}, interpolated from .env (mode 600, gitignored) |
CODER_OIDC_SCOPES |
openid,profile,email |
CODER_OIDC_SIGN_IN_TEXT |
Sign in with loca.zone SSO |
Deliberately unset:
CODER_DISABLE_PASSWORD_AUTH— password login stays as break-glass. If Authelia or the OIDC client breaks, the Owner can still log in with the credential in.admin-credentialsCODER_OIDC_EMAIL_DOMAIN— no domain restriction; the Authelia user database is the gate
Agent and CLI traffic is unaffected: both authenticate with Coder-Session-Token bearer tokens against /api/v2/..., a path OIDC never touches. Verified after the change — ws1 agent main still connected, token GET /api/v2/users/me still 200.
Verification actually performed:
GET /api/v2/users/authmethodsreturnspassword.enabled: trueandoidc.enabled: truewithsignInText: "Sign in with loca.zone SSO"https://code.loca.zone/loginrenders the SSO button next to the password form- clicking it produces the chain
/api/v2/users/oidc/callback?redirect=%2F→https://auth.loca.zone/api/oidc/authorization?...client_id=coder...redirect_uri=https%3A%2F%2Fcode.loca.zone%2Fapi%2Fv2%2Fusers%2Foidc%2Fcallback&response_type=code&scope=openid+profile+emailwith PKCES256→ the Authelia login form at?flow=openid_connect. Authelia raising the form rather thaninvalid_clientis the proof that the client id and secret digest are accepted - the final credential entry at the Authelia form is the operator's own SSO login and was not performed here:
users_database.ymlstores only a password hash, so no automated agent can complete it. First interactive SSO login remains an operator action, and it will create a second Coder account unless the OIDC email matchesadmin@loca.zone
Rotation: regenerate with authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 64 --random.charset alphanumeric, install the Digest to the digest file, put the Random Password in .env, then docker compose restart Authelia and docker compose up -d --force-recreate coder.