--- title: "๐Ÿ” Auth model" description: "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." source: "verified deployment on the loca operator host, 2026-08-19; first-user bootstrap verified 2026-08-20" reviewed: 2026-08-20 --- # ๐Ÿ” Auth model ## ๐Ÿงพ Decision - Coder is the identity provider for its own deployment; the `code.loca.zone` vhost has no `auth_request` gate 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_policy` is `bypass`, 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_session` cookie with a `302` to `https://auth.loca.zone` - what that breaks, concretely: - the `coder` CLI, which authenticates with the `Coder-Session-Token` header and has no cookie jar; `coder login`, `coder templates push`, `coder ssh` and `coder port-forward` all receive a redirect where they expect JSON - workspace agents, which dial `CODER_ACCESS_URL` and hold long-lived `/api/v2` websockets; 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 - 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/v2` would 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`, so `authelia_session` is presented on requests to `code.loca.zone` as well - Coder ignores it; it reads only its own `coder_session_token`, which Coder sets host-scoped for `code.loca.zone` - nothing writes both names, so no overwrite, no truncation and no cross-application session confusion - practical consequence: being logged into `auth.loca.zone` grants 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`, email `admin@loca.zone`, role Owner, status active - organization `coder`, the default one, id `9c28ebd1-4fe6-446b-83e2-f723954fec10` - created with `POST /api/v2/users/first` against `http://127.0.0.1:52226`, so the credential never traversed the public vhost - `GET /api/v2/users/first` flipped from `404` to `200` at that moment, which is upstream's own existence check for the initial user - 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-credentials` at 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 - 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/` returns `200` with no auth redirect - `curl -sf http://127.0.0.1:52226/api/v2/buildinfo` reports `v2.35.3+65e2bfb` with `dashboard_url` `https://code.loca.zone` ## ๐Ÿ”’ No open signup - `POST /api/v2/users/first` is the only account-creating endpoint reachable without authentication and it exists solely to create the initial Owner - `POST /api/v2/users` requires authentication, so every later account is created by an admin, either with `coder users create` or 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 ` only works when run on the machine running the control plane, which here means inside the `coder` container ## ๐Ÿ”— 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: false` - `client_secret` is a `pbkdf2-sha512` digest read via `{{ secret "/config/secrets/oidc/coder-client-secret.digest" }}`; that file is `600 root:root`, and the container runs as root so the strict mode is safe - `authorization_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`; grant `authorization_code`; response type `code` 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-credentials` - `CODER_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/authmethods` returns `password.enabled: true` and `oidc.enabled: true` with `signInText: "Sign in with loca.zone SSO"` - `https://code.loca.zone/login` renders 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+email` with PKCE `S256` โ†’ the Authelia login form at `?flow=openid_connect`. Authelia raising the form rather than `invalid_client` is 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.yml` stores 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 matches `admin@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`. ## ๐Ÿ“š Sources - https://coder.com/docs/reference/api/authentication - https://coder.com/docs/reference/api/users - https://coder.com/docs/admin/users - https://coder.com/docs/admin/users/oidc-auth - https://coder.com/docs/admin/networking - https://coder.com/docs/admin/setup