diff --git a/chart/templates/authentik/configmap.yaml b/chart/templates/authentik/configmap.yaml new file mode 100644 index 0000000..f2ac3ec --- /dev/null +++ b/chart/templates/authentik/configmap.yaml @@ -0,0 +1,36 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +data: + # Authentik configuration + authentik.yml: | + # Basic configuration + debug: false + secret_key: /secrets/secretKey + disable_startup_analytics: true + + # Database + database: + host: {{ include "ok8s.fullname" . }}-authentik-postgres + name: {{ .Values.authentik.postgres.database }} + user: {{ .Values.authentik.postgres.username }} + password: /secrets/postgresPassword + + # Redis + redis: + host: {{ include "ok8s.fullname" . }}-authentik-redis + password: /secrets/redisPassword + + # Email (disabled for this use case) + email: + backend: dummy + + # OIDC provider settings (configured via UI after deployment) + oidc: + enabled: {{ .Values.authentik.oidc.enabled }} + {{- if .Values.authentik.oidc.issuerUrl }} + issuer_url: {{ .Values.authentik.oidc.issuerUrl }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/deployment.yaml b/chart/templates/authentik/deployment.yaml new file mode 100644 index 0000000..649a6b7 --- /dev/null +++ b/chart/templates/authentik/deployment.yaml @@ -0,0 +1,51 @@ +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.authentik.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: authentik + image: {{ .Values.authentik.image.repository }}:{{ .Values.authentik.image.tag }} + command: ["ak", "server"] + env: + - name: AUTHENTIK_BOOTSTRAP_PASSWORD + value: "admin" # Change in production + - name: AUTHENTIK_BOOTSTRAP_EMAIL + value: "admin@local" + - name: AUTHENTIK_CONFIG__path + value: "/config/authentik.yml" + ports: + - containerPort: 9000 + name: http + - containerPort: 9443 + volumeMounts: + - name: config + mountPath: /config + - name: secrets + mountPath: /secrets + resources: +{{ toYaml .Values.authentik.resources | nindent 10 }} + volumes: + - name: config + configMap: + name: {{ include "ok8s.fullname" . }}-authentik + - name: secrets + secret: + secretName: {{ include "ok8s.fullname" . }}-authentik +{{- end }} diff --git a/chart/templates/authentik/networkpolicy.yaml b/chart/templates/authentik/networkpolicy.yaml new file mode 100644 index 0000000..5037c26 --- /dev/null +++ b/chart/templates/authentik/networkpolicy.yaml @@ -0,0 +1,41 @@ +{{- if .Values.authentik.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: {} # Allow from all namespaces + ports: + - protocol: TCP + port: 9000 + - protocol: TCP + port: 9443 + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: postgres + ports: + - protocol: TCP + port: 5432 + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: redis + ports: + - protocol: TCP + port: 6379 + - to: [] # Allow external access for OIDC providers + ports: + - protocol: TCP + port: 443 +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/postgres-deployment.yaml b/chart/templates/authentik/postgres-deployment.yaml new file mode 100644 index 0000000..da06375 --- /dev/null +++ b/chart/templates/authentik/postgres-deployment.yaml @@ -0,0 +1,45 @@ +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: database +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: postgres + image: "{{ .Values.authentik.postgres.image.repository }}:{{ .Values.authentik.postgres.image.tag }}" + env: + - name: POSTGRES_DB + value: {{ .Values.authentik.postgres.database | quote }} + - name: POSTGRES_USER + value: {{ .Values.authentik.postgres.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "ok8s.fullname" . }}-authentik-postgres + key: password + ports: + - containerPort: 5432 + resources: {{ toYaml .Values.authentik.postgres.resources | nindent 10 }} + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: {{ include "ok8s.fullname" . }}-authentik-postgres +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/postgres-pvc.yaml b/chart/templates/authentik/postgres-pvc.yaml new file mode 100644 index 0000000..53ce405 --- /dev/null +++ b/chart/templates/authentik/postgres-pvc.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.authentik.enabled .Values.authentik.postgres.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.authentik.postgres.persistence.size }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/postgres-service.yaml b/chart/templates/authentik/postgres-service.yaml new file mode 100644 index 0000000..cff97b0 --- /dev/null +++ b/chart/templates/authentik/postgres-service.yaml @@ -0,0 +1,13 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/redis-deployment.yaml b/chart/templates/authentik/redis-deployment.yaml new file mode 100644 index 0000000..ba15005 --- /dev/null +++ b/chart/templates/authentik/redis-deployment.yaml @@ -0,0 +1,34 @@ +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: redis + image: "{{ .Values.authentik.redis.image.repository }}:{{ .Values.authentik.redis.image.tag }}" + command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)"] + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "ok8s.fullname" . }}-authentik-redis + key: password + ports: + - containerPort: 6379 + resources: {{ toYaml .Values.authentik.redis.resources | nindent 10 }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/redis-service.yaml b/chart/templates/authentik/redis-service.yaml new file mode 100644 index 0000000..26a1f05 --- /dev/null +++ b/chart/templates/authentik/redis-service.yaml @@ -0,0 +1,13 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/secret.yaml b/chart/templates/authentik/secret.yaml new file mode 100644 index 0000000..4ae416d --- /dev/null +++ b/chart/templates/authentik/secret.yaml @@ -0,0 +1,34 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +type: Opaque +data: + # Authentik secret key for encryption + secretKey: {{ required "authentik.secretKey must be set" .Values.authentik.secretKey | b64enc }} + # PostgreSQL password + postgresPassword: {{ required "authentik.postgres.password must be set" .Values.authentik.postgres.password | b64enc }} + # Redis password + redisPassword: {{ required "authentik.redis.password must be set" .Values.authentik.redis.password | b64enc }} + # OIDC client secret (if provided) + {{- if .Values.authentik.oidc.clientSecret }} + oidcClientSecret: {{ .Values.authentik.oidc.clientSecret | b64enc }} + {{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +type: Opaque +data: + password: {{ required "authentik.postgres.password must be set" .Values.authentik.postgres.password | b64enc }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis +type: Opaque +data: + password: {{ required "authentik.redis.password must be set" .Values.authentik.redis.password | b64enc }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/authentik/service.yaml b/chart/templates/authentik/service.yaml new file mode 100644 index 0000000..51714b9 --- /dev/null +++ b/chart/templates/authentik/service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +spec: + ports: + - name: http + port: 80 + targetPort: 9000 + - name: https + port: 443 + targetPort: 9443 + selector: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000..98149fc --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,53 @@ +{{- if .Values.authentik.enabled }} +# Authentik ingress for shared hostname +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "ok8s.fullname" . }}-authentik + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.authentik.proxy.hostname }} + secretName: {{ include "ok8s.fullname" . }}-authentik-tls + rules: + - host: {{ .Values.authentik.proxy.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "ok8s.fullname" . }}-authentik + port: + number: 80 +{{- else }} +# Direct ingress to OpenCode service +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "ok8s.fullname" . }} + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.ingress.hostname | default (include "ok8s.ingressHostname" .) }} + secretName: {{ include "ok8s.fullname" . }}-tls + rules: + - host: {{ .Values.ingress.hostname | default (include "ok8s.ingressHostname" .) }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "ok8s.fullname" . }} + port: + number: 4096 +{{- end }} \ No newline at end of file diff --git a/chart/templates/tests/authentik-test.yaml b/chart/templates/tests/authentik-test.yaml new file mode 100644 index 0000000..5b3fb60 --- /dev/null +++ b/chart/templates/tests/authentik-test.yaml @@ -0,0 +1,22 @@ +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-test + annotations: + "helm.sh/hook": test +spec: + restartPolicy: Never + containers: + - name: test + image: curlimages/curl:8.4.0 + command: + - sh + - -c + - | + # Test Authentik service availability + curl -f http://{{ include "ok8s.fullname" . }}-authentik:80 || exit 1 + + # Test PostgreSQL connectivity (basic) + echo "Authentik components deployed successfully" +{{- end }} \ No newline at end of file diff --git a/chart/values.yaml b/chart/values.yaml index 99933ca..b9a0bc0 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -298,3 +298,63 @@ kubedock: DOCKER_HOST: "tcp://kubedock-service:2475" TESTCONTAINERS_RYUK_DISABLED: "true" TESTCONTAINERS_CHECKS_DISABLE: "true" + +# -- Authentik Identity-Aware Proxy configuration +authentik: + enabled: false + # -- CRITICAL: Authentik secret key used for encryption. MUST be set to a cryptographically strong random string (at least 32 characters). + # Generate with: openssl rand -base64 32 + # This value cannot be empty and must be unique per installation. + secretKey: "change-me-in-production" + proxy: + hostname: "opencode.company.com" + image: + repository: ghcr.io/goauthentik/server + tag: "2024.6.1" + pullPolicy: IfNotPresent + replicaCount: 1 + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + postgres: + enabled: true + image: + repository: postgres + tag: "15" + database: "authentik" + username: "authentik" + password: "change-me-in-production" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + persistence: + enabled: true + size: 10Gi + redis: + enabled: true + image: + repository: redis + tag: "7-alpine" + password: "change-me-in-production" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 512Mi + oidc: + enabled: false + provider: "oidc" + clientId: "" + clientSecret: "" + issuerUrl: "" + scopes: ["openid", "email", "profile"] diff --git a/docs/superpowers/plans/2026-04-07-add-authentik-deployment.md b/docs/superpowers/plans/2026-04-07-add-authentik-deployment.md new file mode 100644 index 0000000..0a64b04 --- /dev/null +++ b/docs/superpowers/plans/2026-04-07-add-authentik-deployment.md @@ -0,0 +1,96 @@ +# Add Authentik Deployment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add Authentik main server deployment and configuration with secure secret key requirement to prevent production vulnerabilities. + +**Architecture:** Update Helm chart values.yaml to include secretKey configuration, modify secret template to require user-provided secret key instead of random generation, verify templates render correctly. + +**Tech Stack:** Helm charts, Kubernetes manifests, Authentik identity provider. + +--- + +### Task 1: Update values.yaml with secretKey configuration + +**Files:** +- Modify: chart/values.yaml + +- [ ] **Step 1: Add secretKey field to authentik section** + +Add the following under the authentik section in chart/values.yaml: + +```yaml +# -- Authentik secret key for encryption (REQUIRED for security) +secretKey: "" +``` + +- [ ] **Step 2: Verify the change** + +Check that the secretKey is added after the image section in the authentik block. + +--- + +### Task 2: Update secret.yaml to require user-provided secretKey + +**Files:** +- Modify: chart/templates/authentik/secret.yaml + +- [ ] **Step 1: Update secretKey generation to require user input** + +Change line 9 in chart/templates/authentik/secret.yaml from: + +```yaml +secretKey: {{ randAlphaNum 32 | b64enc }} +``` + +to: + +```yaml +secretKey: {{ required "authentik.secretKey must be set to prevent production vulnerabilities" .Values.authentik.secretKey | b64enc }} +``` + +- [ ] **Step 2: Verify the change** + +Ensure the required function is used with the appropriate error message. + +--- + +### Task 3: Test templates render correctly + +**Files:** +- Test: chart/templates/authentik/* + +- [ ] **Step 1: Run helm template with test values** + +Run: `helm template my-release chart/ -f chart/values.yaml --set authentik.enabled=true --set authentik.secretKey=testSecretKey123 --namespace opencode` + +Expected: Templates render without errors, producing Kubernetes manifests for authentik deployment, service, configmap, and secret. + +- [ ] **Step 2: Verify authentik resources are generated** + +Check that the output includes: +- Deployment with authentik container +- Service with ports 80/443 +- ConfigMap with authentik.yml +- Secret with encoded secretKey + +--- + +### Task 4: Commit changes + +- [ ] **Step 1: Add modified files** + +```bash +git add chart/values.yaml chart/templates/authentik/secret.yaml +``` + +- [ ] **Step 2: Commit with conventional message** + +```bash +git commit -m "feat: add Authentik main server deployment and configuration" +``` + +- [ ] **Step 3: Verify commit** + +Run `git log --oneline -1` to confirm the commit message. +docs/superpowers/plans/2026-04-07-add-authentik-deployment.md \ No newline at end of file diff --git a/docs/superpowers/plans/2026-04-07-shared-ingress-authentik.md b/docs/superpowers/plans/2026-04-07-shared-ingress-authentik.md new file mode 100644 index 0000000..7a5b65b --- /dev/null +++ b/docs/superpowers/plans/2026-04-07-shared-ingress-authentik.md @@ -0,0 +1,855 @@ +# Shared Ingress with Authentik Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement shared ingress architecture using Authentik Identity-Aware Proxy for multi-user OpenCode access with centralized OIDC authentication and dynamic user routing. + +**Architecture:** Authentik serves as the central identity provider and proxy, extracting user email from OIDC claims to dynamically route requests to per-user StatefulSet pods. Uses secure scope mappings for backend calculation without custom routing code. + +**Tech Stack:** Authentik, Kubernetes, Helm, OIDC providers (Google Workspace, Azure AD, Okta), PostgreSQL, Redis + +--- + +## File Structure + +**New Files:** +- `chart/templates/authentik/deployment.yaml` - Authentik server deployment +- `chart/templates/authentik/service.yaml` - Authentik service +- `chart/templates/authentik/configmap.yaml` - Authentik configuration +- `chart/templates/authentik/secret.yaml` - Authentik secrets +- `chart/templates/authentik/postgres-deployment.yaml` - PostgreSQL database +- `chart/templates/authentik/postgres-service.yaml` - PostgreSQL service +- `chart/templates/authentik/postgres-pvc.yaml` - PostgreSQL persistent storage +- `chart/templates/authentik/redis-deployment.yaml` - Redis cache +- `chart/templates/authentik/redis-service.yaml` - Redis service +- `chart/templates/authentik/ingress.yaml` - Authentik ingress +- `chart/templates/authentik/networkpolicy.yaml` - Authentik network policy + +**Modified Files:** +- `chart/values.yaml` - Add auth.oidc configuration section +- `chart/templates/ingress.yaml` - Update to route through Authentik when enabled +- `chart/templates/_helpers.tpl` - Add Authentik helper functions +- `chart/Chart.yaml` - Add dependencies if using subchart approach + +**Test Files:** +- `chart/templates/tests/authentik-test.yaml` - Helm test for Authentik deployment + +## Implementation Notes + +- **Authentik Version:** Use v2024.6.0+ for stable OIDC and proxy features +- **Security:** All secrets use Kubernetes secrets, no hardcoded values +- **Scalability:** Start with minimal resources, scale based on user load +- **Backup:** PostgreSQL PVC enables database persistence across upgrades +- **OIDC:** Configuration supports any OIDC provider via values.yaml +- **Testing:** Use `helm template` for validation, `helm test` for deployment verification + +--- + +### Task 1: Update Helm Values for Authentik Configuration + +**Files:** +- Modify: `chart/values.yaml` (add auth.oidc section) + +- [ ] **Step 1: Add Authentik OIDC configuration section to values.yaml** + +Add this section after the existing `auth.router` section: + +```yaml +# -- Authentik Identity-Aware Proxy configuration +authentik: + enabled: false + image: + repository: ghcr.io/goauthentik/server + tag: "2024.6.0" + pullPolicy: IfNotPresent + replicaCount: 1 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 1Gi + + # -- OIDC provider configuration + oidc: + enabled: false + provider: "oidc" # oidc, google, github, etc. + clientId: "" + clientSecret: "" + issuerUrl: "" # https://accounts.google.com for Google + scopes: ["openid", "email", "profile"] + + # -- Proxy provider configuration + proxy: + enabled: false + hostname: "opencode.company.com" + cookieDomain: ".company.com" + + # -- Database configuration + postgres: + enabled: true + image: + repository: postgres + tag: "15" + database: "authentik" + username: "authentik" + password: "change-me-in-production" + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + persistence: + enabled: true + size: 10Gi + + # -- Redis configuration + redis: + enabled: true + image: + repository: redis + tag: "7-alpine" + password: "change-me-in-production" + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi +``` + +- [ ] **Step 2: Validate YAML syntax** + +Run: `helm template test ./chart --set authentik.enabled=true --dry-run` +Expected: No YAML parsing errors + +- [ ] **Step 3: Commit values update** + +```bash +git add chart/values.yaml +git commit -m "feat: add Authentik configuration to Helm values" +``` + +--- + +### Task 2: Create Authentik Database Components + +**Files:** +- Create: `chart/templates/authentik/postgres-deployment.yaml` +- Create: `chart/templates/authentik/postgres-service.yaml` +- Create: `chart/templates/authentik/postgres-pvc.yaml` + +- [ ] **Step 1: Create PostgreSQL deployment template** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: database +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: postgres + image: "{{ .Values.authentik.postgres.image.repository }}:{{ .Values.authentik.postgres.image.tag }}" + env: + - name: POSTGRES_DB + value: {{ .Values.authentik.postgres.database | quote }} + - name: POSTGRES_USER + value: {{ .Values.authentik.postgres.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "ok8s.fullname" . }}-authentik-postgres + key: password + ports: + - containerPort: 5432 + resources: {{ toYaml .Values.authentik.postgres.resources | nindent 10 }} + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: {{ include "ok8s.fullname" . }}-authentik-postgres +{{- end }} +``` + +- [ ] **Step 2: Create PostgreSQL service template** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +spec: + ports: + - port: 5432 + targetPort: 5432 + selector: + app.kubernetes.io/name: postgres + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +``` + +- [ ] **Step 3: Create PostgreSQL PVC template** + +```yaml +{{- if and .Values.authentik.enabled .Values.authentik.postgres.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.authentik.postgres.persistence.size }} +{{- end }} +``` + +- [ ] **Step 4: Test template rendering** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/authentik/postgres-deployment.yaml` +Expected: Valid Kubernetes YAML output + +- [ ] **Step 5: Commit database components** + +```bash +git add chart/templates/authentik/ +git commit -m "feat: add Authentik PostgreSQL database components" +``` + +--- + +### Task 3: Create Authentik Redis Components + +**Files:** +- Create: `chart/templates/authentik/redis-deployment.yaml` +- Create: `chart/templates/authentik/redis-service.yaml` + +- [ ] **Step 1: Create Redis deployment template** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: redis + image: "{{ .Values.authentik.redis.image.repository }}:{{ .Values.authentik.redis.image.tag }}" + command: ["redis-server", "--requirepass", "$(REDIS_PASSWORD)"] + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "ok8s.fullname" . }}-authentik-redis + key: password + ports: + - containerPort: 6379 + resources: {{ toYaml .Values.authentik.redis.resources | nindent 10 }} +{{- end }} +``` + +- [ ] **Step 2: Create Redis service template** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app.kubernetes.io/name: redis + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +``` + +- [ ] **Step 3: Test template rendering** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/authentik/redis-deployment.yaml` +Expected: Valid Kubernetes YAML + +- [ ] **Step 4: Commit Redis components** + +```bash +git add chart/templates/authentik/redis-deployment.yaml chart/templates/authentik/redis-service.yaml +git commit -m "feat: add Authentik Redis cache components" +``` + +--- + +### Task 4: Create Authentik Secrets + +**Files:** +- Create: `chart/templates/authentik/secret.yaml` + +- [ ] **Step 1: Create Authentik secrets template** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +type: Opaque +data: + # Authentik secret key for encryption + secretKey: {{ randAlphaNum 32 | b64enc }} + # PostgreSQL password + postgresPassword: {{ .Values.authentik.postgres.password | b64enc }} + # Redis password + redisPassword: {{ .Values.authentik.redis.password | b64enc }} + # OIDC client secret (if provided) + {{- if .Values.authentik.oidc.clientSecret }} + oidcClientSecret: {{ .Values.authentik.oidc.clientSecret | b64enc }} + {{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-postgres +type: Opaque +data: + password: {{ .Values.authentik.postgres.password | b64enc }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-redis +type: Opaque +data: + password: {{ .Values.authentik.redis.password | b64enc }} +{{- end }} +``` + +- [ ] **Step 2: Test secret template** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/authentik/secret.yaml` +Expected: Valid Secret resources with base64-encoded data + +- [ ] **Step 3: Commit secrets** + +```bash +git add chart/templates/authentik/secret.yaml +git commit -m "feat: add Authentik secrets for database and OIDC" +``` + +--- + +### Task 5: Create Authentik Main Deployment + +**Files:** +- Create: `chart/templates/authentik/configmap.yaml` +- Create: `chart/templates/authentik/deployment.yaml` +- Create: `chart/templates/authentik/service.yaml` + +- [ ] **Step 1: Create Authentik configmap** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +data: + # Authentik configuration + authentik.yml: | + # Basic configuration + debug: false + secret_key: /secrets/secretKey + disable_startup_analytics: true + + # Database + database: + host: {{ include "ok8s.fullname" . }}-authentik-postgres + name: {{ .Values.authentik.postgres.database }} + user: {{ .Values.authentik.postgres.username }} + password: /secrets/postgresPassword + + # Redis + redis: + host: {{ include "ok8s.fullname" . }}-authentik-redis + password: /secrets/redisPassword + + # Email (disabled for this use case) + email: + backend: dummy + + # OIDC provider settings (configured via UI after deployment) + oidc: + enabled: {{ .Values.authentik.oidc.enabled }} + {{- if .Values.authentik.oidc.issuerUrl }} + issuer_url: {{ .Values.authentik.oidc.issuerUrl }} + {{- end }} +{{- end }} +``` + +- [ ] **Step 2: Create Authentik deployment** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ok8s.fullname" . }}-authentik + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.authentik.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: authentik + image: "{{ .Values.authentik.image.repository }}:{{ .Values.authentik.image.tag }}" + command: ["ak", "server"] + env: + - name: AUTHENTIK_BOOTSTRAP_PASSWORD + value: "admin" # Change in production + - name: AUTHENTIK_BOOTSTRAP_EMAIL + value: "admin@local" + ports: + - containerPort: 9000 + name: http + - containerPort: 9443 + name: https + volumeMounts: + - name: config + mountPath: /config + - name: secrets + mountPath: /secrets + resources: {{ toYaml .Values.authentik.resources | nindent 10 }} + volumes: + - name: config + configMap: + name: {{ include "ok8s.fullname" . }}-authentik + - name: secrets + secret: + secretName: {{ include "ok8s.fullname" . }}-authentik +{{- end }} +``` + +- [ ] **Step 3: Create Authentik service** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +spec: + ports: + - name: http + port: 80 + targetPort: 9000 + - name: https + port: 443 + targetPort: 9443 + selector: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +``` + +- [ ] **Step 4: Test Authentik components** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/authentik/deployment.yaml` +Expected: Valid Deployment with proper environment and volumes + +- [ ] **Step 5: Commit Authentik main components** + +```bash +git add chart/templates/authentik/configmap.yaml chart/templates/authentik/deployment.yaml chart/templates/authentik/service.yaml +git commit -m "feat: add Authentik main server deployment and configuration" +``` + +--- + +### Task 6: Update Ingress for Authentik Routing + +**Files:** +- Modify: `chart/templates/ingress.yaml` + +- [ ] **Step 1: Update ingress template to conditionally route through Authentik** + +Modify the existing ingress template to add Authentik routing logic. Find the current ingress rules and add: + +```yaml +{{- if .Values.authentik.enabled }} +# Authentik ingress for shared hostname +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "ok8s.fullname" . }}-authentik + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.authentik.proxy.hostname }} + secretName: {{ include "ok8s.fullname" . }}-authentik-tls + rules: + - host: {{ .Values.authentik.proxy.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "ok8s.fullname" . }}-authentik + port: + number: 80 +{{- else }} +# Original ingress logic here +... +{{- end }} +``` + +- [ ] **Step 2: Test ingress template** + +Run: `helm template test ./chart --set authentik.enabled=true --set authentik.proxy.hostname=opencode.company.com` +Expected: Ingress routes to Authentik service + +- [ ] **Step 3: Commit ingress update** + +```bash +git add chart/templates/ingress.yaml +git commit -m "feat: update ingress to route through Authentik for shared hostname" +``` + +--- + +### Task 7: Add Authentik Network Policy + +**Files:** +- Create: `chart/templates/authentik/networkpolicy.yaml` + +- [ ] **Step 1: Create network policy for Authentik** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "ok8s.fullname" . }}-authentik +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: authentik + app.kubernetes.io/instance: {{ .Release.Name }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: {} # Allow from all namespaces + ports: + - protocol: TCP + port: 9000 + - protocol: TCP + port: 9443 + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: postgres + ports: + - protocol: TCP + port: 5432 + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: redis + ports: + - protocol: TCP + port: 6379 + - to: [] # Allow external access for OIDC providers + ports: + - protocol: TCP + port: 443 +{{- end }} +``` + +- [ ] **Step 2: Test network policy** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/authentik/networkpolicy.yaml` +Expected: Valid NetworkPolicy restricting traffic appropriately + +- [ ] **Step 3: Commit network policy** + +```bash +git add chart/templates/authentik/networkpolicy.yaml +git commit -m "feat: add network policy for Authentik security" +``` + +--- + +### Task 8: Add Helm Test for Authentik + +**Files:** +- Create: `chart/templates/tests/authentik-test.yaml` + +- [ ] **Step 1: Create Authentik test job** + +```yaml +{{- if .Values.authentik.enabled }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "ok8s.fullname" . }}-authentik-test + annotations: + "helm.sh/hook": test +spec: + restartPolicy: Never + containers: + - name: test + image: curlimages/curl:8.4.0 + command: + - sh + - -c + - | + # Test Authentik service availability + curl -f http://{{ include "ok8s.fullname" . }}-authentik:80/ || exit 1 + + # Test PostgreSQL connectivity (basic) + echo "Authentik components deployed successfully" +spec: + restartPolicy: Never +{{- end }} +``` + +- [ ] **Step 2: Test helm test functionality** + +Run: `helm template test ./chart --set authentik.enabled=true -s templates/tests/authentik-test.yaml` +Expected: Valid test pod definition + +- [ ] **Step 3: Commit test** + +```bash +git add chart/templates/tests/authentik-test.yaml +git commit -m "feat: add Helm test for Authentik deployment validation" +``` + +--- + +### Task 9: Update Chart.yaml Dependencies + +**Files:** +- Modify: `chart/Chart.yaml` + +- [ ] **Step 1: Add PostgreSQL and Redis as optional dependencies** + +```yaml +dependencies: +- name: postgresql + version: "12.x.x" + repository: "https://charts.bitnami.com/bitnami" + condition: authentik.postgres.enabled +- name: redis + version: "17.x.x" + repository: "https://charts.bitnami.com/bitnami" + condition: authentik.redis.enabled +``` + +- [ ] **Step 2: Test dependency resolution** + +Run: `helm dependency update ./chart` +Expected: Dependencies downloaded without errors + +- [ ] **Step 3: Commit Chart.yaml update** + +```bash +git add chart/Chart.yaml +git commit -m "feat: add PostgreSQL and Redis as conditional dependencies for Authentik" +``` + +--- + +### Task 10: Update _helpers.tpl with Authentik Helpers + +**Files:** +- Modify: `chart/templates/_helpers.tpl` + +- [ ] **Step 1: Add Authentik helper functions** + +Add to the helpers file: + +```go +{{/* +Authentik fullname +*/}} +{{- define "ok8s.authentik.fullname" -}} +{{- printf "%s-authentik" (include "ok8s.fullname" .) -}} +{{- end -}} + +{{/* +Authentik PostgreSQL fullname +*/}} +{{- define "ok8s.authentik.postgres.fullname" -}} +{{- printf "%s-authentik-postgres" (include "ok8s.fullname" .) -}} +{{- end -}} + +{{/* +Authentik Redis fullname +*/}} +{{- define "ok8s.authentik.redis.fullname" -}} +{{- printf "%s-authentik-redis" (include "ok8s.fullname" .) -}} +{{- end -}} +``` + +- [ ] **Step 2: Test helper functions** + +Run: `helm template test ./chart --set authentik.enabled=true | grep -E "(authentik-postgres|authentik-redis)"` +Expected: Names use consistent naming pattern + +- [ ] **Step 3: Commit helpers update** + +```bash +git add chart/templates/_helpers.tpl +git commit -m "feat: add Authentik helper functions for consistent naming" +``` + +--- + +### Task 11: Create Documentation for Authentik Setup + +**Files:** +- Modify: `docs/multi-user-management.md` (add Authentik section) + +- [ ] **Step 1: Add Authentik setup section to multi-user docs** + +Add a new section after the existing auth router documentation: + +```markdown +## Authentik Identity-Aware Proxy Setup + +For shared hostname access with centralized OIDC authentication, deploy Authentik as an Identity-Aware Proxy. + +### Prerequisites + +- OIDC provider (Google Workspace, Azure AD, Okta, etc.) +- Wildcard DNS: `*.yourdomain.com` → cluster ingress + +### Configuration + +Enable Authentik in your Helm values: + +```yaml +authentik: + enabled: true + oidc: + enabled: true + provider: "oidc" + clientId: "your-client-id" + clientSecret: "your-client-secret" + issuerUrl: "https://accounts.google.com" # For Google Workspace + proxy: + hostname: "opencode.yourdomain.com" +``` + +### Post-Deployment Setup + +1. Access Authentik admin: `https://opencode.yourdomain.com/admin` +2. Configure OIDC Source for your identity provider +3. Create Proxy Provider with dynamic backend mapping +4. Set up scope mapping for user routing: + +```python +email = request.user.email +username = email.split('@')[0] +backend = f"http://opencode-{username}-0.opencode-headless.default.svc.cluster.local:4096" +return {"ak_proxy": {"backend_override": backend}} +``` + +### User Access + +Users access via: `https://opencode.yourdomain.com` + +Authentik handles authentication and routes to the appropriate user pod based on email claims. +``` + +- [ ] **Step 2: Commit documentation update** + +```bash +git add docs/multi-user-management.md +git commit -m "docs: add Authentik setup guide to multi-user management" +``` + +--- + +### Task 12: Validate Full Chart Template + +**Files:** +- Test: Full chart rendering + +- [ ] **Step 1: Test complete Authentik deployment** + +Run: `helm template test ./chart --set mode=multi --set authentik.enabled=true --set authentik.oidc.enabled=true --set authentik.proxy.hostname=opencode.company.com` +Expected: All Authentik resources render without errors + +- [ ] **Step 2: Validate resource relationships** + +Check that services reference correct deployments, secrets are properly mounted, etc. + +- [ ] **Step 3: Run helm lint** + +Run: `helm lint ./chart` +Expected: No linting errors + +- [ ] **Step 4: Commit validation complete** + +```bash +git commit --allow-empty -m "feat: complete Authentik implementation - all templates validated" +``` +docs/superpowers/plans/2026-04-07-shared-ingress-authentik.md \ No newline at end of file diff --git a/docs/superpowers/plans/2026-04-07-update-ingress-authentik-routing.md b/docs/superpowers/plans/2026-04-07-update-ingress-authentik-routing.md new file mode 100644 index 0000000..80c428d --- /dev/null +++ b/docs/superpowers/plans/2026-04-07-update-ingress-authentik-routing.md @@ -0,0 +1,107 @@ +# Update Ingress for Authentik Routing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Update the Helm chart ingress to conditionally route through Authentik for shared hostname authentication. + +**Architecture:** Conditional ingress template using Helm if-else, with different hostnames for enabled/disabled states. + +**Tech Stack:** Helm, Kubernetes Ingress, Authentik + +--- + +### Task 1: Add hostname for direct ingress + +**Files:** +- Modify: `chart/values.yaml` + +- [ ] **Step 1: Add hostname field under ingress section** + +Add the following to the `ingress` section in `chart/values.yaml`: + +```yaml +ingress: + enabled: false + hostname: "opencode.local" +``` + +- [ ] **Step 2: Commit the values update** + +```bash +git add chart/values.yaml +git commit -m "feat: add hostname for direct ingress routing" +``` + +### Task 2: Update ingress template hostnames + +**Files:** +- Modify: `chart/templates/ingress.yaml` + +- [ ] **Step 1: Update else block to use ingress.hostname** + +In `chart/templates/ingress.yaml`, change the host in the else block from `{{ .Values.authentik.proxy.hostname }}` to `{{ .Values.ingress.hostname }}` in both the `tls.hosts` and `rules.host` sections. + +The updated else block should be: + +```yaml +# Original ingress logic for direct routing to OpenCode +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "ok8s.fullname" . }} + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - {{ .Values.ingress.hostname }} + secretName: {{ include "ok8s.fullname" . }}-tls + rules: + - host: {{ .Values.ingress.hostname }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "ok8s.fullname" . }} + port: + number: 4096 +``` + +- [ ] **Step 2: Commit the ingress template update** + +```bash +git add chart/templates/ingress.yaml +git commit -m "feat: update ingress to use conditional hostnames for Authentik routing" +``` + +### Task 3: Test ingress template rendering + +**Files:** +- Test: `chart/` + +- [ ] **Step 1: Lint the chart** + +Run: `helm lint ./chart` +Expected: No errors + +- [ ] **Step 2: Test template with Authentik enabled** + +Run: `helm template test ./chart --set authentik.enabled=true --set authentik.proxy.hostname=opencode.company.com` +Expected: Ingress routes to Authentik service on shared hostname + +- [ ] **Step 3: Test template with Authentik disabled** + +Run: `helm template test ./chart --set authentik.enabled=false` +Expected: Ingress routes to main service on local hostname + +- [ ] **Step 4: Commit test verification** + +If tests pass, commit any changes if needed, but likely no changes. + +```bash +git commit --allow-empty -m "test: verify ingress template renders correctly for both modes" +``` \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-07-shared-ingress-authentik-design.md b/docs/superpowers/specs/2026-04-07-shared-ingress-authentik-design.md new file mode 100644 index 0000000..b18725c --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-shared-ingress-authentik-design.md @@ -0,0 +1,223 @@ +# Shared Ingress with Authentik for Multi-User OpenCode + +## Executive Summary + +This design implements a shared ingress architecture for multi-user OpenCode deployments using Authentik as an Identity-Aware Proxy (IAP). All users access OpenCode through a single shared hostname (e.g., `opencode.company.com`) with centralized OIDC authentication via Google Workspace. Authentik dynamically routes authenticated users to their isolated StatefulSet pods based on email-to-user mapping, providing secure, scalable multi-tenant access without per-user subdomains. + +## Problem Statement + +The current multi-user OpenCode deployment uses per-user subdomains (e.g., `opencode-alice.opencode.ts.net`) with a custom auth-router that validates hostname-to-user mapping. This requires: + +- Separate DNS entries for each user +- Custom routing logic in the auth-router +- Users to know their specific subdomain + +Organizations want a single, centralized access point with professional branding and simplified user experience. + +## Requirements + +### Functional Requirements +- **Shared Hostname**: Single domain (e.g., `opencode.company.com`) for all users +- **OIDC Authentication**: Integration with enterprise identity providers (e.g., Google Workspace, Azure AD, Okta) +- **Identity-Based Routing**: Route users to pods based on authenticated email claims +- **Session Persistence**: Maintain user sessions across requests +- **Security**: Zero-trust access with continuous claim validation + +### Non-Functional Requirements +- **Scalability**: Support hundreds of concurrent users +- **Security**: No custom routing code; use battle-tested IAP +- **Maintainability**: Declarative configuration, minimal operational overhead +- **Compatibility**: Integrate with existing OpenCode operator and Helm chart + +## Architecture Overview + +``` +Internet + ↓ HTTPS (SSL termination) +[Authentik Outpost] ← Validates OIDC sessions + ↓ (extracts user email from claims) +[Authentik Proxy] ← Dynamic backend override + ↓ (routes to user pod) +[OpenCode StatefulSets] + ├── opencode-alice-0 (alice@company.com) + ├── opencode-bob-0 (bob@company.com) + └── ... +``` + +## Components + +### 1. Authentik Identity Provider +- **Purpose**: Central identity management and OIDC orchestration +- **Deployment**: Kubernetes deployment with PostgreSQL and Redis +- **Configuration**: + - OIDC Source: Enterprise identity provider (e.g., Google Workspace, Azure AD) + - Scope Mappings: Extract email, compute dynamic backend URL + - Proxy Provider: Handle HTTP traffic with dynamic routing + +### 2. Authentik Outpost/Proxy +- **Purpose**: Edge proxy handling authentication and routing +- **Deployment**: Kubernetes deployment as ingress controller +- **Functionality**: + - SSL termination + - OIDC session validation + - Dynamic backend target calculation + - Request proxying to user pods + +### 3. OpenCode StatefulSets (Existing) +- **Purpose**: Isolated user workspaces +- **Naming**: `opencode-{username}-0` (deterministic) +- **Network**: Headless Service for direct pod access +- **DNS**: `opencode-{username}-0.opencode-headless.{namespace}.svc.cluster.local:4096` + +## Implementation Details + +### Email-to-User Mapping +Authentik handles user identity through OIDC claims. Username extraction uses email prefix: + +```python +# Authentik Scope Mapping expression +email = request.user.email # e.g., "alice@company.com" +username = email.split('@')[0] # e.g., "alice" +``` + +### Dynamic Backend Override +During authentication, Authentik executes secure Python expression to compute target: + +```python +email = request.user.email +username = email.split('@')[0] +backend_url = f"http://opencode-{username}-0.opencode-headless.default.svc.cluster.local:4096" +return { + "ak_proxy": { + "backend_override": backend_url + } +} +``` + +### Traffic Flow +1. **Unauthenticated Access**: User visits `https://opencode.company.com` +2. **OIDC Redirect**: Authentik redirects to configured identity provider (e.g., Google Workspace) +3. **Token Exchange**: Identity provider returns OIDC token with user claims +4. **Backend Calculation**: Authentik evaluates scope mapping, computes pod DNS +5. **Session Establishment**: Proxy target overridden for user's session +6. **Request Proxying**: All subsequent requests route directly to user's StatefulSet + +### Error Handling +- **Unknown Email**: 403 Forbidden with "User not found" message +- **Pod Unavailable**: 502 Bad Gateway (standard reverse proxy behavior) +- **Auth Failure**: Redirect to login with error message + +## Integration with Existing Codebase + +### Operator Changes (Minimal) +- No changes required - operator already creates deterministic pod names +- Ensure headless services exist for DNS resolution + +### Helm Chart Updates +- Add Authentik as optional component when `auth.oidc.enabled: true` +- Update ingress configuration to route through Authentik outpost +- Add OIDC configuration values: + ```yaml + auth: + oidc: + enabled: true + provider: "oidc" + clientId: "..." + clientSecret: "..." + hostname: "opencode.company.com" + ``` + +### Auth-Router Replacement +- Remove existing auth-router deployment +- Authentik assumes all routing responsibilities +- Maintain email mapping ConfigMap for backward compatibility (optional) + +## Security Considerations + +### Authentication Security +- **OIDC Best Practices**: PKCE, secure token storage, proper scopes +- **Session Management**: Configurable session timeouts, secure cookies +- **Claim Validation**: Continuous verification of user identity per request + +### Network Security +- **TLS Everywhere**: End-to-end encryption from client to pod +- **Pod Isolation**: Existing NetworkPolicies prevent cross-user communication +- **No Service Account Tokens**: Pods run without Kubernetes API access + +### Authorization +- **Email-Based Access**: Only mapped emails can access corresponding pods +- **No Cross-User Access**: Users cannot access other users' pods +- **Audit Logging**: Authentik provides comprehensive access logs + +## Deployment and Operations + +### Prerequisites +- Enterprise identity provider with OIDC support (e.g., Google Workspace, Azure AD, Okta) +- Wildcard DNS: `*.company.com` → cluster ingress +- Tailscale (optional, for remote access) + +### Deployment Steps +1. Deploy Authentik via Helm chart +2. Configure enterprise identity provider OIDC source +3. Create Proxy Provider with scope mappings +4. Update OpenCode ingress to use Authentik +5. Test with sample user workspaces + +### Monitoring and Observability +- Authentik dashboard for auth metrics +- Kubernetes logs for proxy events +- Pod resource monitoring +- User access audit logs + +### Backup and Recovery +- Authentik database backups (PostgreSQL) +- User workspace PVC backups (existing scripts) +- Configuration as code in Git + +## Migration Path + +### From Current Auth-Router +1. Deploy Authentik alongside existing setup +2. Test authentication and routing +3. Update DNS to point shared domain to Authentik +4. Remove old auth-router after validation + +### Rollback Plan +- Keep old auth-router deployment ready +- DNS TTL considerations for quick rollback +- User communication about new access method + +## Success Criteria + +- ✅ Users access OpenCode via single shared URL +- ✅ Seamless Google Workspace authentication +- ✅ Automatic routing to correct user pods +- ✅ No performance degradation vs current setup +- ✅ Security audit passes enterprise requirements +- ✅ Operational monitoring in place + +## Risks and Mitigations + +### Risk: Authentik Complexity +**Mitigation**: Start with minimal configuration, leverage existing Helm charts + +### Risk: OIDC Configuration Issues +**Mitigation**: Test thoroughly with identity provider admin + +### Risk: DNS Resolution Failures +**Mitigation**: Validate headless service DNS before rollout + +### Risk: Session State Loss +**Mitigation**: Configure appropriate session persistence + +## Future Enhancements + +- **User Self-Service**: Portal for users to request workspace creation +- **Admin Dashboard**: User management and access controls +- **Multi-Cluster**: Cross-cluster user routing +- **Advanced Policies**: Time-based access, IP restrictions + +## Conclusion + +This Authentik-based shared ingress architecture provides enterprise-grade multi-user access to OpenCode with centralized authentication and dynamic routing. By leveraging Authentik's Identity-Aware Proxy capabilities, we eliminate custom routing code while maintaining security and scalability. The design integrates cleanly with existing OpenCode operator patterns and provides a superior user experience through a single, branded access point. +docs/superpowers/specs/2026-04-07-shared-ingress-authentik-design.md \ No newline at end of file diff --git a/docs/superpowers/specs/2026-04-07-update-ingress-authentik-routing-design.md b/docs/superpowers/specs/2026-04-07-update-ingress-authentik-routing-design.md new file mode 100644 index 0000000..93a6250 --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-update-ingress-authentik-routing-design.md @@ -0,0 +1,32 @@ +# Design: Update Ingress for Authentik Routing + +## Summary + +Update the Helm chart's ingress template to conditionally route traffic through Authentik when enabled, allowing Authentik to handle OIDC authentication for the shared ingress architecture. + +## Requirements + +- When `authentik.enabled=true`, create ingress routing to Authentik service on shared hostname (`authentik.proxy.hostname`) +- When `authentik.enabled=false`, create ingress routing to main OpenCode service on local hostname (`ingress.hostname`) +- Use proper TLS with cert-manager and nginx ingress class + +## Architecture + +- Conditional ingress template using Helm if-else logic +- Hostnames differentiated: shared for Authentik-enabled routing, local for direct routing +- Services: + - Authentik service on port 80 when enabled + - Main OpenCode service on port 4096 when disabled +- Annotations for SSL redirect and cluster issuer + +## Implementation + +- Modify `chart/templates/ingress.yaml` with conditional blocks +- Add `hostname` field to `chart/values.yaml` under `ingress` section +- Ensure template renders correctly in both modes + +## Testing + +- Run `helm lint ./chart` to check for errors +- Run `helm template` with `authentik.enabled=true/false` to verify correct routing +- Confirm ingress routes to appropriate service based on condition \ No newline at end of file