diff --git a/apps/auth/README.md b/apps/auth/README.md deleted file mode 100644 index 743f515..0000000 --- a/apps/auth/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Auth (Authentik + Kyverno SSO annotation policy) - -Draft manifests only — nothing applied yet. Convention: any Ingress annotated -with `use-sso-auth: "true"` gets the Authentik forward-auth annotations injected -by the Kyverno ClusterPolicy (`auth/sso-auth-policy.yaml`). - -## Deploy order - -1. **Authentik** (`auth/authentik.yaml`) — IdP + embedded outpost. Needs a - Postgres user/DB on postgresql.haven first (see below), plus secrets from - Vaultwarden/ESO. -2. **Proxy provider** — create in Authentik UI (or via API) once it's up: - one provider per app you want header-auth'd, mode "forward_auth (single - app)", or a domain-level provider with the embedded outpost. -3. **Kyverno** (`auth/kyverno-helm.yaml`) — policy engine. -4. **SSO policy** (`auth/sso-auth-policy.yaml`) — expands `use-sso-auth: "true"` - into `auth-url` / `auth-signin` / `auth-response-headers` annotations. -5. Annotate ingresses: `kubectl annotate ingress -n use-sso-auth=true` - and mirror the change in this repo. - -## Postgres bootstrap (one-time, on postgresql.haven) - -```sql -CREATE USER authentik WITH PASSWORD ''; -CREATE DATABASE authentik OWNER authentik; -``` - -## Notes - -- Outpost URL used everywhere is `https://auth.haven` — add DNS + ingress for - the outpost before annotating anything. -- Apps that should *consume* the injected `X-Authentik-*` headers (Grafana auth - proxy, Paperless remote-user, OpenWebUI trusted headers) need their own env - changes; those are app-side, not covered by the policy. -- API/WebSocket-heavy apps (arr stack, qBittorrent) should NOT get the - annotation on API paths — either skip the annotation or exclude paths. - -## Which apps get the annotation (scoping) - -Three tiers, applied per ingress — never blanket: - -1. **OIDC tier** (no annotation, wire native OIDC in the app instead): - grafana, paperless, affine, openwebui, vaultwarden, beszel, slink. - Proper logout, group mapping, zero API breakage. -2. **Forward-auth tier** (`use-sso-auth: "true"`): browser-only utilities with - no real auth — it-tools, notepad, searxng, homepage, archivebox, - stirlingpdf, file-nginx, code-config, havenllo, own apps (chacal, - mindforge) if wanted. -3. **Hands off** (built-in auth is fine, forward-auth breaks clients): - sonarr, radarr, prowlarr, qbittorrent, adguard, changedetection, - uptimekuma, cloudreve (WebDAV), jellyfin (clients can't do redirects — - only the community SSO plugin route exists). - -## Pre-annotation checklist (per new app) - -Forward-auth breakage is loud and immediate if you look in the right places. -Before annotating any real hostname: - -1. **Ask: what talks to this app that isn't a human in a browser?** - Mobile/desktop apps with own login, API-key consumers, push/webhook - receivers, cronjobs curling through the ingress, WebDAV/RSS → any of - those = hands off (tier 3) or exclude paths. "Just me in a browser" = - safe. -2. **Canary first:** create a scratch ingress for the same service - (`-test.haven`) with the annotation; leave the original untouched - for ~a week. Forgotten integrations keep hitting the original and only - break the canary — instant rollback - (`kubectl annotate ingress -n use-sso-auth-`). -3. **Grep the docs** (30s): "reverse proxy", "trusted header", "API key", - "webhook", "basic auth". A "running behind a reverse proxy" doc section - is where landmines are documented. -4. **Header-trust hygiene:** apps consuming `X-Authentik-*` must be - reachable ONLY through the ingress (no NodePort/exposed port, otherwise - LAN clients can forge headers straight to the pod). Keep built-in local - login enabled; never set SSO-only mode. - -## Files - -- `kyverno-helm.yaml` — Kyverno install values -- `sso-auth-policy.yaml` — ClusterPolicy: `use-sso-auth` → nginx auth annotations -- `authentik.yaml` — Authentik server/worker + outpost, Postgres PVC, ingress - -## Status / when to actually deploy this (decision from 2026-08-28) - -NOT deployed — deliberate. Sole user, low login frequency, everything -reachable via wg-easy, so SSO solves a problem that doesn't exist. The -`*.ivanch.me` public ingresses were reviewed instead: exposure without usage -is pure risk, and the right fix is removing exposure (VPN-only), not adding -an IdP. - -Pull the trigger only if one of these becomes true: - -1. A second person uses the homelab regularly (SSO value scales with users). -2. Something must be exposed that can't sit behind VPN (third-party - webhooks/callbacks) — then public tier gets `use-sso-auth` + 2FA. -3. Login friction on the OIDC apps (grafana/paperless/affine/openwebui/ - vaultwarden/beszel/slink) actually annoys on a weekly basis. - -Until then this folder is a shelf-ready draft; cost of keeping it is zero. diff --git a/apps/auth/authentik.yaml b/apps/auth/authentik.yaml deleted file mode 100644 index afdf840..0000000 --- a/apps/auth/authentik.yaml +++ /dev/null @@ -1,244 +0,0 @@ -# Authentik — IdP + embedded outpost (proxy provider forward-auth). -# Draft — do NOT apply as-is: -# - secret values are placeholders; create them from Vaultwarden/ESO first -# - requires the authentik user/DB on postgresql.haven (see README.md) -# - requires DNS record for auth.haven -> ingress IP (user manages DNS) ---- -apiVersion: v1 -kind: Secret -metadata: - name: authentik-secrets - namespace: auth -type: Opaque -stringData: - postgres_password: "CHANGE_ME" # Vaultwarden -> ESO later - secret_key: "CHANGE_ME" # `openssl rand -base64 60` - bootstrap_password: "CHANGE_ME" # initial akadmin password - bootstrap_token: "CHANGE_ME" # outpost token ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: authentik-media - namespace: auth -spec: - accessModes: [ReadWriteOnce] - storageClassName: nfs-fast-client # verified: exists in cluster (nfs-client is also default) - resources: - requests: - storage: 2Gi ---- -# Server (web + API) -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authentik-server - namespace: auth -spec: - replicas: 1 - selector: - matchLabels: { app: authentik, component: server } - template: - metadata: - labels: { app: authentik, component: server } - spec: - containers: - - name: server - image: ghcr.io/goauthentik/server:2026.8 - args: [server] - env: - - name: AUTHENTIK_SECRET_KEY - valueFrom: - { secretKeyRef: { name: authentik-secrets, key: secret_key } } - - name: AUTHENTIK_POSTGRESQL__HOST - value: postgresql.haven - - name: AUTHENTIK_POSTGRESQL__NAME - value: authentik - - name: AUTHENTIK_POSTGRESQL__USER - value: authentik - - name: AUTHENTIK_POSTGRESQL__PASSWORD - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: postgres_password }, - } - - name: AUTHENTIK_BOOTSTRAP_PASSWORD - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: bootstrap_password }, - } - - name: AUTHENTIK_BOOTSTRAP_TOKEN - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: bootstrap_token }, - } - - name: AUTHENTIK_ERROR_REPORTING__ENABLED - value: "false" - ports: - - { containerPort: 9000, name: http } - - { containerPort: 9443, name: https } - readinessProbe: - httpGet: { path: /-/health/ready/, port: 9000 } - initialDelaySeconds: 20 - periodSeconds: 10 - livenessProbe: - httpGet: { path: /-/health/live/, port: 9000 } - initialDelaySeconds: 40 - periodSeconds: 20 - resources: - requests: { cpu: 250m, memory: 512Mi } - limits: { memory: 1Gi, cpu: 1000m } - volumeMounts: - - { name: media, mountPath: /media } - volumes: - - name: media - persistentVolumeClaim: { claimName: authentik-media } ---- -# Worker (policies, outpost management, scheduled tasks) -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authentik-worker - namespace: auth -spec: - replicas: 1 - selector: - matchLabels: { app: authentik, component: worker } - template: - metadata: - labels: { app: authentik, component: worker } - spec: - containers: - - name: worker - image: ghcr.io/goauthentik/server:2026.8 - args: [worker] - env: # same env as server; kept duplicated for a flat draft manifest - - name: AUTHENTIK_SECRET_KEY - valueFrom: - { secretKeyRef: { name: authentik-secrets, key: secret_key } } - - name: AUTHENTIK_POSTGRESQL__HOST - value: postgresql.haven - - name: AUTHENTIK_POSTGRESQL__NAME - value: authentik - - name: AUTHENTIK_POSTGRESQL__USER - value: authentik - - name: AUTHENTIK_POSTGRESQL__PASSWORD - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: postgres_password }, - } - - name: AUTHENTIK_BOOTSTRAP_PASSWORD - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: bootstrap_password }, - } - - name: AUTHENTIK_BOOTSTRAP_TOKEN - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: bootstrap_token }, - } - - name: AUTHENTIK_ERROR_REPORTING__ENABLED - value: "false" - resources: - requests: { cpu: 200m, memory: 512Mi } - limits: { memory: 1Gi, cpu: 1000m } ---- -apiVersion: v1 -kind: Service -metadata: - name: authentik - namespace: auth -spec: - selector: { app: authentik, component: server } - ports: - - { name: http, port: 80, targetPort: 9000 } ---- -# Embedded outpost — runs the proxy providers; this is what ingress-nginx -# calls for /auth/nginx on every use-sso-auth ingress. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authentik-outpost - namespace: auth -spec: - replicas: 1 - selector: - matchLabels: { app: authentik, component: outpost } - template: - metadata: - labels: { app: authentik, component: outpost } - spec: - containers: - - name: outpost - image: ghcr.io/goauthentik/proxy:2026.8 - env: - - name: AUTHENTIK_HOST - value: http://authentik.auth.svc.cluster.local - - name: AUTHENTIK_INSECURE - value: "true" - - name: AUTHENTIK_TOKEN - valueFrom: - { - secretKeyRef: - { name: authentik-secrets, key: bootstrap_token }, - } - ports: - - { containerPort: 9000, name: http } - readinessProbe: - httpGet: { path: /outpost.goauthentik.io/ping, port: 9000 } - initialDelaySeconds: 10 - periodSeconds: 10 - resources: - requests: { cpu: 200m, memory: 512Mi } - limits: { memory: 1Gi, cpu: 1000m } ---- -apiVersion: v1 -kind: Service -metadata: - name: authentik-outpost - namespace: auth -spec: - selector: { app: authentik, component: outpost } - ports: - - { name: http, port: 80, targetPort: 9000 } ---- -# SSO portal (user-facing login) — auth.haven -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: authentik - namespace: auth -spec: - ingressClassName: traefik - rules: - - host: auth.haven - http: - paths: - - path: / - pathType: Prefix - backend: { service: { name: authentik, port: { number: 80 } } } ---- -# Outpost route — the /outpost.goauthentik.io path must resolve on the same -# host the protected apps redirect to. Kept as a separate ingress so it can -# also be attached to other hosts later if needed (do NOT give it -# use-sso-auth — that would create an auth loop). -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: authentik-outpost - namespace: auth -spec: - ingressClassName: traefik - rules: - - host: auth.haven - http: - paths: - - path: /outpost.goauthentik.io - pathType: Prefix - backend: - { service: { name: authentik-outpost, port: { number: 80 } } } diff --git a/apps/auth/kyverno-helm.yaml b/apps/auth/kyverno-helm.yaml deleted file mode 100644 index 00ff76b..0000000 --- a/apps/auth/kyverno-helm.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Kyverno — policy engine. Install with: -# helm repo add kyverno https://kyverno.github.io/kyverno -# helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace -f auth/kyverno-helm.yaml -# The only consumer of Kyverno here is the use-sso-auth mutation policy; -# if you later drop it, Kyverno can be removed with no other impact. -admissionController: - replicas: 1 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - memory: 256Mi -backgroundController: - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi -cleanupController: - enabled: false -reportsController: - enabled: false diff --git a/apps/auth/sso-auth-policy.yaml b/apps/auth/sso-auth-policy.yaml deleted file mode 100644 index 4326bef..0000000 --- a/apps/auth/sso-auth-policy.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# ClusterPolicy: expands the single `use-sso-auth: "true"` Ingress annotation -# into the ingress-nginx forward-auth annotations pointed at the Authentik -# embedded outpost. -# -# Test before applying to real ingresses: -# kubectl apply -f auth/sso-auth-policy.yaml -# kubectl annotate ingress -n default homepage use-sso-auth=true --dry-run=server -o yaml # check annotations get injected -# # remove the test annotation afterwards ---- -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: inject-sso-auth -spec: - validationFailureAction: Audit - background: false - rules: - - name: authentik-forward-auth - match: - any: - - resources: - kinds: [Ingress] - preconditions: - all: - - key: "{{ request.object.metadata.annotations.\"use-sso-auth\" || '' }}" - operator: Equals - value: "true" - mutate: - patchStrategicMerge: - metadata: - annotations: - nginx.ingress.kubernetes.io/auth-url: https://auth.haven/outpost.goauthentik.io/auth/nginx - nginx.ingress.kubernetes.io/auth-signin: https://auth.haven/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri - # NOTE: no auth-snippet used — ingress-nginx 1.15 rejects snippet - # annotations by default (allow-snippet-annotations=false, your - # controller ConfigMap is empty so it uses the default). - # ingress-nginx already forwards X-Original-URL to the auth - # backend automatically, which is all Authentik needs. - nginx.ingress.kubernetes.io/auth-response-headers: X-Authentik-Username,X-Authentik-Email,X-Authentik-Groups,X-Authentik-Name,X-Authentik-Metadata