Files
coder/q5vault/operations/auth-model.md

6.5 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.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 <username> only works when run on the machine running the control plane, which here means inside the coder container

🔭 Future upgrade path: Authelia as an OIDC provider

Not implemented, and it requires operator approval because it means editing the shared Authelia deployment.

  • shape of the change:
    • register a new OIDC client in Authelia for Coder, with redirect URI https://code.loca.zone/api/v2/users/oidc/callback
    • set CODER_OIDC_ISSUER_URL, CODER_OIDC_CLIENT_ID, CODER_OIDC_CLIENT_SECRET and, if wanted, CODER_OIDC_EMAIL_DOMAIN on the coder service
    • optionally add offline_access to CODER_OIDC_SCOPES so sessions survive access-token expiry
    • optionally set CODER_DISABLE_PASSWORD_AUTH=true once OIDC login is proven
  • what it does and does not change:
    • it unifies identity inside Coder, at the application layer, where the CLI and agents keep using their bearer tokens
    • it does not, and must not, add an nginx auth_request gate; the failure modes above still apply
  • prerequisites the operator must approve: a new Authelia client definition, a client secret placed in .env, and a restart of both stacks

📚 Sources