coder: autostop TTL, nightly backups, Authelia OIDC, wildcard runbook
- 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
This commit is contained in:
@@ -30,6 +30,14 @@ services:
|
|||||||
CODER_ACCESS_URL: https://code.loca.zone
|
CODER_ACCESS_URL: https://code.loca.zone
|
||||||
CODER_PG_CONNECTION_URL: postgresql://coder:${CODER_PG_PASSWORD}@database:5432/coder?sslmode=disable
|
CODER_PG_CONNECTION_URL: postgresql://coder:${CODER_PG_PASSWORD}@database:5432/coder?sslmode=disable
|
||||||
CODER_TELEMETRY_ENABLE: "false"
|
CODER_TELEMETRY_ENABLE: "false"
|
||||||
|
# Authelia OIDC. Password login stays enabled as break-glass, so
|
||||||
|
# CODER_DISABLE_PASSWORD_AUTH is deliberately unset. No email-domain
|
||||||
|
# restriction: the Authelia user database is the gate.
|
||||||
|
CODER_OIDC_ISSUER_URL: https://auth.loca.zone
|
||||||
|
CODER_OIDC_CLIENT_ID: coder
|
||||||
|
CODER_OIDC_CLIENT_SECRET: ${CODER_OIDC_CLIENT_SECRET}
|
||||||
|
CODER_OIDC_SCOPES: openid,profile,email
|
||||||
|
CODER_OIDC_SIGN_IN_TEXT: "Sign in with loca.zone SSO"
|
||||||
# No CODER_WILDCARD_ACCESS_URL. Nested names such as foo.code.loca.zone
|
# No CODER_WILDCARD_ACCESS_URL. Nested names such as foo.code.loca.zone
|
||||||
# DO resolve via the *.loca.zone wildcard; the actual blocker is TLS
|
# DO resolve via the *.loca.zone wildcard; the actual blocker is TLS
|
||||||
# issuance: this lineage covers only code.loca.zone and
|
# issuance: this lineage covers only code.loca.zone and
|
||||||
|
|||||||
@@ -60,19 +60,43 @@ reviewed: 2026-08-20
|
|||||||
- there is no self-service registration in Coder OSS with password auth; a user appears only because an admin made it
|
- 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
|
- 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
|
## 🔗 Authelia as OIDC provider — configured
|
||||||
|
|
||||||
Not implemented, and it requires operator approval because it means editing the shared Authelia deployment.
|
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.
|
||||||
|
|
||||||
- shape of the change:
|
Authelia side, `/home/loca/dev/services/authelia/config/configuration.yml`, second entry under `identity_providers.oidc.clients`:
|
||||||
- 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
|
- `client_id: 'coder'`, `client_name: 'Coder'`, `public: false`
|
||||||
- optionally add `offline_access` to `CODER_OIDC_SCOPES` so sessions survive access-token expiry
|
- `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
|
||||||
- optionally set `CODER_DISABLE_PASSWORD_AUTH=true` once OIDC login is proven
|
- `authorization_policy: 'one_factor'`, `consent_mode: 'implicit'`, `token_endpoint_auth_method: 'client_secret_basic'`
|
||||||
- what it does and does not change:
|
- single redirect URI `https://code.loca.zone/api/v2/users/oidc/callback`
|
||||||
- it unifies identity inside Coder, at the application layer, where the CLI and agents keep using their bearer tokens
|
- scopes `openid`, `profile`, `email`, `groups`; grant `authorization_code`; response type `code`
|
||||||
- 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
|
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
|
## 📚 Sources
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,35 @@ ss -ltn
|
|||||||
- any dev server started from a workspace or an IDE terminal must avoid every port in the AGENTS.md registry; `52222` pwiz, `52223` reserved for pwiz, `52224` cdsr and `52226` code are taken
|
- any dev server started from a workspace or an IDE terminal must avoid every port in the AGENTS.md registry; `52222` pwiz, `52223` reserved for pwiz, `52224` cdsr and `52226` code are taken
|
||||||
- loca.zone application backends bind loopback inside `51000-53000`
|
- loca.zone application backends bind loopback inside `51000-53000`
|
||||||
|
|
||||||
|
## 1️⃣3️⃣ Autostop TTL is armed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(sed -n 's/^session_token=//p' /home/loca/dev/coder/.admin-credentials)
|
||||||
|
curl -s -H "Coder-Session-Token: $TOKEN" http://127.0.0.1:52226/api/v2/templates \
|
||||||
|
| python3 -c "import json,sys; [print(t['name'], t['default_ttl_ms'], t['activity_bump_ms']) for t in json.load(sys.stdin)]"
|
||||||
|
curl -s -H "Coder-Session-Token: $TOKEN" http://127.0.0.1:52226/api/v2/users/loca/workspace/ws1 \
|
||||||
|
| python3 -c "import json,sys; w=json.load(sys.stdin); print(w['ttl_ms'], w['latest_build']['deadline'])"
|
||||||
|
```
|
||||||
|
|
||||||
|
- expect template `docker` at `28800000` default TTL (8h) and `3600000` activity bump (1h)
|
||||||
|
- expect `ws1` `ttl_ms` `28800000` and a non-null `deadline` roughly 8h ahead
|
||||||
|
- a `deadline` of `None` on a running workspace means that build predates the TTL: the schedule only attaches at build time, so `coder restart ws1 -y` is required once. `coder schedule extend` refuses with `Workspace shutdown is manual.` in that state
|
||||||
|
- the subcommand is `coder schedule stop <ws> 8h`; there is no `override-stop` in v2.35.3
|
||||||
|
|
||||||
|
## 1️⃣4️⃣ Nightly backup timer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl list-timers stacks-backup --no-pager
|
||||||
|
sudo -n systemctl start stacks-backup.service
|
||||||
|
D=/home/loca/backups/stacks/$(date +%F)
|
||||||
|
sudo -n gunzip -t $D/coder-db.sql.gz && sudo -n ls -l $D
|
||||||
|
```
|
||||||
|
|
||||||
|
- expect the timer `active` with a next elapse at 04:30 local, `Persistent=true` so a missed run catches up after downtime
|
||||||
|
- expect three non-empty artifacts in the dated directory: `coder-db.sql.gz` (`pg_dump` of the Coder control plane), `continue-config.tgz` (`/home/loca/.continue`, which lives outside both projects), `stack-configs.tgz` (both stacks' config, deploy, scripts and templates)
|
||||||
|
- artifacts are root-owned; retention keeps 14 dated directories
|
||||||
|
- `.env`, `.admin-credentials` and `.ai-credentials` are deliberately excluded — they are keys, not data; verify with `sudo -n tar tzf $D/stack-configs.tgz | grep -c credentials` returning `0`
|
||||||
|
|
||||||
## 🧹 Pre-publish wiki lint gate
|
## 🧹 Pre-publish wiki lint gate
|
||||||
|
|
||||||
Run this before every `build.sh` invocation, so before item 7 above. It is a gate, not a smoke test: it exits non-zero and publishes nothing.
|
Run this before every `build.sh` invocation, so before item 7 above. It is a gate, not a smoke test: it exits non-zero and publishes nothing.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: "🚧 Deliberate limits"
|
title: "🚧 Deliberate limits"
|
||||||
description: "What this Coder deployment does not enable and what each omission costs: no wildcard access URL, no Authelia gate, no open signup, no external provisioner, and a hard memory pressure gate."
|
description: "What this Coder deployment does not enable and what each omission costs: no wildcard access URL, no Authelia gate, no open signup, no external provisioner, and a hard memory pressure gate."
|
||||||
source: "verified deployment on the loca operator host, 2026-08-19"
|
source: "verified deployment on the loca operator host, 2026-08-19"
|
||||||
reviewed: 2026-08-19
|
reviewed: 2026-08-20
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🚧 Deliberate limits
|
# 🚧 Deliberate limits
|
||||||
@@ -28,19 +28,54 @@ Every entry here is a choice, not an oversight. Each one states the cost, the tr
|
|||||||
- `coder port-forward` from the CLI
|
- `coder port-forward` from the CLI
|
||||||
- SSH port forwarding, including `coder ssh -R` and anything set up by `coder config-ssh`
|
- SSH port forwarding, including `coder ssh -R` and anything set up by `coder config-ssh`
|
||||||
- Coder Desktop and the IDE remotes (VS Code extension, JetBrains Gateway)
|
- Coder Desktop and the IDE remotes (VS Code extension, JetBrains Gateway)
|
||||||
- documented upgrade path, all of it requiring operator approval:
|
|
||||||
- obtain a DNS-01 wildcard certificate for `*.code.loca.zone`, which means adding DNS-provider API credentials to this host
|
### Runbook: enabling `*.code.loca.zone` (blocked, do not start until both prerequisites exist)
|
||||||
- add an nginx server block for `server_name *.code.loca.zone` proxying to `127.0.0.1:52226`
|
|
||||||
- set `CODER_WILDCARD_ACCESS_URL` on the `coder` service and restart it
|
Preflight re-confirmed 2026-08-20: `sudo -n certbot plugins` lists exactly `nginx`, `standalone`, `webroot`, no `dns-*` plugin is installed, and `/etc/letsencrypt/` holds no DNS credential file. So this runbook cannot be executed today, and nothing below should be attempted piecemeal — a half-applied wildcard leaves nginx serving a name its certificate does not cover.
|
||||||
- once wildcards work, upstream advises disabling path-based apps (`CODER_DISABLE_PATH_APPS=true`), because path apps share the Coder API origin and a malicious workspace could reuse Coder cookies against the API
|
|
||||||
|
Two named prerequisites, both operator decisions:
|
||||||
|
|
||||||
|
1. a DNS-provider API credential file for the `loca.zone` zone, mode 600 root-owned, e.g. `/etc/letsencrypt/dns-<provider>.ini`. The provider is genuinely unknown from this host: `loca.zone` nameservers point at `178.18.250.170`, self-hosted, so the credential shape cannot be guessed
|
||||||
|
2. the matching plugin package `python3-certbot-dns-<provider>`, which is a package install and therefore ask-first
|
||||||
|
|
||||||
|
Then, in order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. expand the existing lineage rather than creating a second one
|
||||||
|
sudo certbot certonly \
|
||||||
|
--dns-<provider> \
|
||||||
|
--dns-<provider>-credentials /etc/letsencrypt/dns-<provider>.ini \
|
||||||
|
--cert-name code.loca.zone --expand \
|
||||||
|
-d code.loca.zone -d wiki.code.loca.zone -d '*.code.loca.zone'
|
||||||
|
sudo certbot certificates # confirm all three SANs on one lineage
|
||||||
|
|
||||||
|
# 2. nginx: add the wildcard to both blocks that own the app name
|
||||||
|
# /etc/nginx/sites-available/code.loca.zone
|
||||||
|
# line 3 (listen 443 ssl) -> server_name code.loca.zone *.code.loca.zone;
|
||||||
|
# line 56 (listen 80) -> server_name code.loca.zone *.code.loca.zone;
|
||||||
|
# leave the two wiki.code.loca.zone blocks alone: a wildcard on the app
|
||||||
|
# block would otherwise swallow the wiki host, and the wiki must stay static
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
|
||||||
|
# 3. Coder: add to the coder service environment in docker-compose.yml
|
||||||
|
# CODER_WILDCARD_ACCESS_URL: "*.code.loca.zone"
|
||||||
|
cd /home/loca/dev/coder && sudo -n docker compose config >/dev/null \
|
||||||
|
&& sudo -n docker compose up -d --force-recreate coder
|
||||||
|
curl -sf http://127.0.0.1:52226/healthz
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified against the running v2.35.3 binary, not assumed: `coder server --help` documents `--wildcard-access-url, $CODER_WILDCARD_ACCESS_URL` taking the form `"*.example.com"`, and `--disable-path-apps, $CODER_DISABLE_PATH_APPS` whose own help text says it is "recommended for security purposes if a `--wildcard-access-url` is configured". So the follow-up hardening step is:
|
||||||
|
|
||||||
|
- set `CODER_DISABLE_PATH_APPS: "true"` only after subdomain apps are confirmed working, because it removes the path-based routes this deployment currently depends on, including the `code-server` app on `ws1`
|
||||||
- note the RFC 1035 label limit: app, agent, workspace and username together must stay under 63 characters in one hostname label
|
- note the RFC 1035 label limit: app, agent, workspace and username together must stay under 63 characters in one hostname label
|
||||||
|
- renewal: the lineage keeps its `--dns-<provider>` authenticator in `/etc/letsencrypt/renewal/code.loca.zone.conf`, so unattended renewal needs the credential file to stay in place; the existing nginx reload hook still applies
|
||||||
|
|
||||||
## 🔓 No Authelia gate
|
## 🔓 No Authelia gate
|
||||||
|
|
||||||
- cost: the dashboard is publicly reachable and Coder's own login page is the only thing between the internet and the deployment; there is no second factor at the proxy
|
- cost: the dashboard is publicly reachable and Coder's own login page is the only thing between the internet and the deployment; there is no second factor at the proxy
|
||||||
- why the cost is accepted: an `auth_request` gate would 302 every cookie-less request, which is exactly what the `coder` CLI, the workspace agents and DERP relay traffic send. A browser-only gate is impossible because the SPA and the agents share `/api/v2`
|
- why the cost is accepted: an `auth_request` gate would 302 every cookie-less request, which is exactly what the `coder` CLI, the workspace agents and DERP relay traffic send. A browser-only gate is impossible because the SPA and the agents share `/api/v2`
|
||||||
- compensating controls: Coder's own session handling, no open signup, a pinned server image, and a single admin account
|
- compensating controls: Coder's own session handling, no open signup, a pinned server image, and a single admin account
|
||||||
- full argument and the OIDC-via-Authelia upgrade path: [[operations/auth-model]]
|
- since 2026-08-20 identity is nonetheless unified: Coder consumes Authelia over OIDC at the application layer, with password login kept as break-glass, while the proxy stays ungated — full argument in [[operations/auth-model]]
|
||||||
|
|
||||||
## 🚫 No open signup
|
## 🚫 No open signup
|
||||||
|
|
||||||
|
|||||||
34
scripts/backup-stacks.sh
Executable file
34
scripts/backup-stacks.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Nightly backup for the dual IDE stacks (cdsr = code-server, code = Coder v2).
|
||||||
|
#
|
||||||
|
# Deliberately NOT backed up: /home/loca/dev/coder/.env and
|
||||||
|
# /home/loca/dev/coder/.admin-credentials, and
|
||||||
|
# /home/loca/dev/code-server/.ai-credentials. Those are keys, not data. Losing
|
||||||
|
# them is recoverable (re-bootstrap the Coder owner via container exec, re-mint
|
||||||
|
# the LiteLLM virtual key); copying them in plaintext into a nightly archive
|
||||||
|
# only widens the exposure surface.
|
||||||
|
#
|
||||||
|
# Runs as root from stacks-backup.service, so no sudo prefixes here.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEST="/home/loca/backups/stacks/$(date +%F)"
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
|
||||||
|
# (a) Coder control-plane database
|
||||||
|
docker compose -f /home/loca/dev/coder/docker-compose.yml exec -T database \
|
||||||
|
pg_dump -U coder coder | gzip > "$DEST/coder-db.sql.gz"
|
||||||
|
|
||||||
|
# (b) Continue's real state (config + chat sessions live outside the project)
|
||||||
|
tar czf "$DEST/continue-config.tgz" -C /home/loca .continue
|
||||||
|
|
||||||
|
# (c) Stack configuration (source of truth for both deployments)
|
||||||
|
tar czf "$DEST/stack-configs.tgz" --exclude .git -C /home/loca/dev \
|
||||||
|
code-server/config code-server/scripts code-server/deploy \
|
||||||
|
coder/docker-compose.yml coder/deploy coder/templates coder/config
|
||||||
|
|
||||||
|
# (d) Retention: keep 14 days of dated directories
|
||||||
|
find /home/loca/backups/stacks -maxdepth 1 -mindepth 1 -type d -mtime +14 \
|
||||||
|
-exec rm -rf {} +
|
||||||
|
|
||||||
|
printf 'backup complete: %s\n' "$DEST"
|
||||||
|
ls -l "$DEST"
|
||||||
Reference in New Issue
Block a user