diff --git a/infrastructure/production/RUNBOOK.md b/infrastructure/production/RUNBOOK.md index 15bf4bc..c98e74f 100644 --- a/infrastructure/production/RUNBOOK.md +++ b/infrastructure/production/RUNBOOK.md @@ -22,6 +22,9 @@ bottom to rebuild it. Per-layer detail lives in `host/README.md`, - **cert-manager** + `letsencrypt-staging` / `letsencrypt-prod` (HTTP-01/Traefik). - **Data tier** (`dezky-data` ns) — Postgres 16, Mongo 7, Redis 7 as StatefulSets on Longhorn PVCs. Postgres holds the `authentik` + `ocis` DBs. +- **Authentik** (`dezky-auth` ns) — live at https://auth.dezky.eu (LE cert), + image `2026.5.2`, on our Postgres/Redis. `akadmin` bootstrap login. +- **Traefik** — global HTTP→HTTPS 308 redirect (`fleet/traefik/`). ## Reproduce from scratch @@ -82,6 +85,19 @@ kubectl apply -k fleet/data/ kubectl -n dezky-data get pods,pvc # all Running, PVCs Bound on longhorn ``` +### 6. Authentik (IdP) +See `fleet/authentik/README.md`. Create `dezky-auth` ns + `authentik-secret` +(DB/Redis pw read back from dezky-data so they match; SECRET_KEY + bootstrap +generated), then `kubectl apply -f fleet/authentik/helmchart.yaml`. Reachable at +https://auth.dezky.eu; first login `akadmin` / `AUTHENTIK_BOOTSTRAP_PASSWORD`. + +### 7. Traefik — global HTTP→HTTPS redirect +```bash +kubectl apply -f fleet/traefik/helmchartconfig.yaml +kubectl -n kube-system delete job helm-install-traefik # force the controller to re-run with merged values +# verify: curl -sI http://auth.dezky.eu -> 308 -> https://auth.dezky.eu/ +``` + ## Secrets — read live values for Bitwarden ```bash @@ -95,8 +111,9 @@ k redis-secret REDIS_PASSWORD ## Still TODO (next layers) -1. **Authentik** (`auth.dezky.eu`) — OIDC for the portal; uses the `authentik` - Postgres DB + Redis. +1. **Authentik** — ✅ deployed (`auth.dezky.eu`). Remaining: OIDC app + blueprints (portal + operator, with prod redirect URLs + client secrets) and + the cosmetic rebrand. See `fleet/authentik/README.md`. 2. **OCIS** (files) — uses the `ocis` Postgres DB + Hetzner Object Storage (S3). 3. **Apps** — `fleet/apps/` (portal · platform-api · booking) + their secrets. 4. **Stalwart** (host) — `host/stalwart/install.sh`; needs DNS + PTR. diff --git a/infrastructure/production/fleet/authentik/README.md b/infrastructure/production/fleet/authentik/README.md new file mode 100644 index 0000000..85406f8 --- /dev/null +++ b/infrastructure/production/fleet/authentik/README.md @@ -0,0 +1,49 @@ +# fleet/authentik — identity provider (auth.dezky.eu) + +Authentik, mirroring the dev docker-compose service but pointed at the +in-cluster data tier. Deployed via the k3s Helm controller (`helmchart.yaml`, +which mirrors `values.yaml`). Live at **https://auth.dezky.eu** (Let's Encrypt). + +- External **Postgres** (`postgres.dezky-data`, db/user `authentik`) + **Redis** + (`redis.dezky-data`) — chart's bundled subcharts disabled. +- Secrets via `global.envFrom` → the `authentik-secret` Secret (generated + on-box; see `secret.example.yaml`). DB/Redis passwords match the dezky-data + secrets. +- Ingress: Traefik + cert-manager `letsencrypt-prod`. +- `error_reporting` off, update-check off, bootstrap email `admin@dezky.eu`. + +## Deploy +```bash +# 1. secret (reads DB/Redis pw from dezky-data so they match; rest generated) +ADB=$(kubectl -n dezky-data get secret postgres-secret -o jsonpath='{.data.AUTHENTIK_DB_PASSWORD}' | base64 -d) +RDB=$(kubectl -n dezky-data get secret redis-secret -o jsonpath='{.data.REDIS_PASSWORD}' | base64 -d) +kubectl create namespace dezky-auth --dry-run=client -o yaml | kubectl apply -f - +kubectl -n dezky-auth create secret generic authentik-secret \ + --from-literal=AUTHENTIK_SECRET_KEY=$(openssl rand -hex 50) \ + --from-literal=AUTHENTIK_POSTGRESQL__PASSWORD="$ADB" \ + --from-literal=AUTHENTIK_REDIS__PASSWORD="$RDB" \ + --from-literal=AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -hex 16) \ + --from-literal=AUTHENTIK_BOOTSTRAP_TOKEN=$(openssl rand -hex 32) +# 2. install +kubectl apply -f helmchart.yaml +kubectl -n dezky-auth rollout status deploy/authentik-server --timeout=300s +``` + +## First login +```bash +# akadmin password (store in Bitwarden): +kubectl -n dezky-auth get secret authentik-secret -o jsonpath='{.data.AUTHENTIK_BOOTSTRAP_PASSWORD}' | base64 -d; echo +``` +Log in at https://auth.dezky.eu as **akadmin** / that password. + +## TODO (mirror the rest of dev — not yet applied) +- **OIDC app blueprints.** Dev provisions the operator app + a + `dezky-platform-admins` access policy via a blueprint + (`infrastructure/docker-compose/configs/authentik/blueprints/`). For prod, + mount the equivalent at `/blueprints/custom` with **prod** redirect URLs + (`operator.dezky.eu`, `app.dezky.eu`) + the matching client IDs/secrets. + The portal OIDC client (hand-made in dev) also needs creating. +- **Rebrand** ("Powered by authentik" → "Powered by Dezky"): dev runs + `rebrand-web.sh` as a root lifecycle script. Replicating in k8s needs a + root-capable initContainer/command override — cosmetic, deferred. +- Pin the **chart version** (currently latest → app `2026.5.2`). diff --git a/infrastructure/production/fleet/authentik/helmchart.yaml b/infrastructure/production/fleet/authentik/helmchart.yaml new file mode 100644 index 0000000..0d2716e --- /dev/null +++ b/infrastructure/production/fleet/authentik/helmchart.yaml @@ -0,0 +1,55 @@ +# Authentik via the k3s Helm controller. valuesContent mirrors values.yaml +# (keep them in sync). Version intentionally unpinned for the first install — +# PIN the resolved chart version here once it's up (see RUNBOOK.md). +# +# The 'authentik-secret' Secret must exist in dezky-auth BEFORE this (it carries +# AUTHENTIK_SECRET_KEY + the DB/Redis/bootstrap creds via global.envFrom). +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: authentik + namespace: kube-system +spec: + repo: https://charts.goauthentik.io + chart: authentik + targetNamespace: dezky-auth + createNamespace: true + valuesContent: |- + image: + tag: "2026.5.2" + global: + envFrom: + - secretRef: + name: authentik-secret + env: + - name: AUTHENTIK_BOOTSTRAP_EMAIL + value: admin@dezky.eu + - name: AUTHENTIK_DISABLE_UPDATE_CHECK + value: "true" + authentik: + error_reporting: + enabled: false + postgresql: + host: postgres.dezky-data + name: authentik + user: authentik + redis: + host: redis.dezky-data + postgresql: + enabled: false + redis: + enabled: false + server: + ingress: + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - auth.dezky.eu + paths: + - "/" + tls: + - hosts: + - auth.dezky.eu + secretName: authentik-tls diff --git a/infrastructure/production/fleet/authentik/secret.example.yaml b/infrastructure/production/fleet/authentik/secret.example.yaml new file mode 100644 index 0000000..02459da --- /dev/null +++ b/infrastructure/production/fleet/authentik/secret.example.yaml @@ -0,0 +1,29 @@ +# Authentik secrets — template. Generate + apply OUT-OF-BAND, store in Bitwarden. +# The DB/Redis passwords MUST equal the ones in the dezky-data secrets +# (postgres-secret.AUTHENTIK_DB_PASSWORD and redis-secret.REDIS_PASSWORD), so the +# create command below reads them back rather than inventing new ones: +# +# ADB=$(kubectl -n dezky-data get secret postgres-secret -o jsonpath='{.data.AUTHENTIK_DB_PASSWORD}' | base64 -d) +# RDB=$(kubectl -n dezky-data get secret redis-secret -o jsonpath='{.data.REDIS_PASSWORD}' | base64 -d) +# kubectl create namespace dezky-auth --dry-run=client -o yaml | kubectl apply -f - +# kubectl -n dezky-auth create secret generic authentik-secret \ +# --from-literal=AUTHENTIK_SECRET_KEY=$(openssl rand -hex 50) \ +# --from-literal=AUTHENTIK_POSTGRESQL__PASSWORD="$ADB" \ +# --from-literal=AUTHENTIK_REDIS__PASSWORD="$RDB" \ +# --from-literal=AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -hex 16) \ +# --from-literal=AUTHENTIK_BOOTSTRAP_TOKEN=$(openssl rand -hex 32) +# +# AUTHENTIK_BOOTSTRAP_PASSWORD = first login for `akadmin` at https://auth.dezky.eu +# AUTHENTIK_BOOTSTRAP_TOKEN = used by platform-api/provisioning to call the API +apiVersion: v1 +kind: Secret +metadata: + name: authentik-secret + namespace: dezky-auth +type: Opaque +stringData: + AUTHENTIK_SECRET_KEY: REPLACE_openssl_rand_hex_50 + AUTHENTIK_POSTGRESQL__PASSWORD: REPLACE_match_dezky-data_AUTHENTIK_DB_PASSWORD + AUTHENTIK_REDIS__PASSWORD: REPLACE_match_dezky-data_REDIS_PASSWORD + AUTHENTIK_BOOTSTRAP_PASSWORD: REPLACE_openssl_rand_hex_16 + AUTHENTIK_BOOTSTRAP_TOKEN: REPLACE_openssl_rand_hex_32 diff --git a/infrastructure/production/fleet/authentik/values.yaml b/infrastructure/production/fleet/authentik/values.yaml new file mode 100644 index 0000000..7740b74 --- /dev/null +++ b/infrastructure/production/fleet/authentik/values.yaml @@ -0,0 +1,55 @@ +# Authentik production Helm values — mirrors the dev docker-compose service +# (ghcr.io/goauthentik/server:2025.10), pointed at the in-cluster data tier. +# +# Secrets come from the 'authentik-secret' Secret via global.envFrom (generated +# on-box; see README) — NEVER in this file. Non-secret config only here. +# +# NOTE: chart version is intentionally unpinned at first install (helm-controller +# pulls latest). After it's up, pin the installed chart + image versions here + +# in RUNBOOK.md for reproducibility. + +image: + tag: "2026.5.2" # deployed version (latest chart as of 2026-06-08) + +global: + # AUTHENTIK_SECRET_KEY, AUTHENTIK_POSTGRESQL__PASSWORD, AUTHENTIK_REDIS__PASSWORD, + # AUTHENTIK_BOOTSTRAP_PASSWORD, AUTHENTIK_BOOTSTRAP_TOKEN + envFrom: + - secretRef: + name: authentik-secret + env: + - name: AUTHENTIK_BOOTSTRAP_EMAIL + value: admin@dezky.eu + - name: AUTHENTIK_DISABLE_UPDATE_CHECK + value: "true" + +authentik: + error_reporting: + enabled: false + postgresql: + host: postgres.dezky-data + name: authentik + user: authentik + redis: + host: redis.dezky-data + +# Use the in-cluster data tier, not the chart's bundled subcharts. +postgresql: + enabled: false +redis: + enabled: false + +server: + ingress: + enabled: true + ingressClassName: traefik + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - auth.dezky.eu + paths: + - "/" + tls: + - hosts: + - auth.dezky.eu + secretName: authentik-tls diff --git a/infrastructure/production/fleet/traefik/helmchartconfig.yaml b/infrastructure/production/fleet/traefik/helmchartconfig.yaml new file mode 100644 index 0000000..d6feef7 --- /dev/null +++ b/infrastructure/production/fleet/traefik/helmchartconfig.yaml @@ -0,0 +1,24 @@ +# Customise the k3s-bundled Traefik: redirect ALL HTTP (:80) → HTTPS (:443) +# globally, for every Ingress on the cluster. +# +# k3s manages Traefik via a HelmChart named 'traefik' in kube-system; a +# HelmChartConfig of the same name MERGES these values into it (k3s re-runs the +# install). We inject the redirect as Traefik static-config args +# (additionalArguments) — version-independent, unlike the chart's +# ports.web.redirectTo value which didn't render on this chart version. +# +# HTTP-01 ACME is unaffected: Let's Encrypt follows the 308 to HTTPS, so +# cert-manager challenges still validate. +apiVersion: helm.cattle.io/v1 +kind: HelmChartConfig +metadata: + name: traefik + namespace: kube-system +spec: + valuesContent: |- + additionalArguments: + # to=:443 (NOT 'websecure') — the websecure entrypoint listens on :8443 + # internally, which isn't exposed; redirect to the public 443 instead. + - "--entrypoints.web.http.redirections.entrypoint.to=:443" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entrypoints.web.http.redirections.entrypoint.permanent=true"