diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d253cc44..00cc6508 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -158,6 +158,7 @@ jobs: --from-literal=CAMELEER_AUTH_TOKEN="$CAMELEER_AUTH_TOKEN" \ --from-literal=CAMELEER_UI_USER="${CAMELEER_UI_USER:-admin}" \ --from-literal=CAMELEER_UI_PASSWORD="${CAMELEER_UI_PASSWORD:-admin}" \ + --from-literal=CAMELEER_JWT_SECRET="${CAMELEER_JWT_SECRET}" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic clickhouse-credentials \ @@ -166,9 +167,27 @@ jobs: --from-literal=CLICKHOUSE_PASSWORD="$CLICKHOUSE_PASSWORD" \ --dry-run=client -o yaml | kubectl apply -f - + kubectl create secret generic authentik-credentials \ + --namespace=cameleer \ + --from-literal=PG_USER="${AUTHENTIK_PG_USER:-authentik}" \ + --from-literal=PG_PASSWORD="${AUTHENTIK_PG_PASSWORD}" \ + --from-literal=AUTHENTIK_SECRET_KEY="${AUTHENTIK_SECRET_KEY}" \ + --dry-run=client -o yaml | kubectl apply -f - + + kubectl create secret generic cameleer-oidc \ + --namespace=cameleer \ + --from-literal=CAMELEER_OIDC_ENABLED="${CAMELEER_OIDC_ENABLED:-false}" \ + --from-literal=CAMELEER_OIDC_ISSUER="${CAMELEER_OIDC_ISSUER}" \ + --from-literal=CAMELEER_OIDC_CLIENT_ID="${CAMELEER_OIDC_CLIENT_ID}" \ + --from-literal=CAMELEER_OIDC_CLIENT_SECRET="${CAMELEER_OIDC_CLIENT_SECRET}" \ + --dry-run=client -o yaml | kubectl apply -f - + kubectl apply -f deploy/clickhouse.yaml kubectl -n cameleer rollout status statefulset/clickhouse --timeout=120s + kubectl apply -f deploy/authentik.yaml + kubectl -n cameleer rollout status deployment/authentik-server --timeout=180s + kubectl apply -f deploy/server.yaml kubectl -n cameleer set image deployment/cameleer3-server \ server=gitea.siegeln.net/cameleer/cameleer3-server:${{ github.sha }} @@ -181,7 +200,15 @@ jobs: env: REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} CAMELEER_AUTH_TOKEN: ${{ secrets.CAMELEER_AUTH_TOKEN }} + CAMELEER_JWT_SECRET: ${{ secrets.CAMELEER_JWT_SECRET }} CAMELEER_UI_USER: ${{ secrets.CAMELEER_UI_USER }} CAMELEER_UI_PASSWORD: ${{ secrets.CAMELEER_UI_PASSWORD }} CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_USER }} CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_PASSWORD }} + AUTHENTIK_PG_USER: ${{ secrets.AUTHENTIK_PG_USER }} + AUTHENTIK_PG_PASSWORD: ${{ secrets.AUTHENTIK_PG_PASSWORD }} + AUTHENTIK_SECRET_KEY: ${{ secrets.AUTHENTIK_SECRET_KEY }} + CAMELEER_OIDC_ENABLED: ${{ secrets.CAMELEER_OIDC_ENABLED }} + CAMELEER_OIDC_ISSUER: ${{ secrets.CAMELEER_OIDC_ISSUER }} + CAMELEER_OIDC_CLIENT_ID: ${{ secrets.CAMELEER_OIDC_CLIENT_ID }} + CAMELEER_OIDC_CLIENT_SECRET: ${{ secrets.CAMELEER_OIDC_CLIENT_SECRET }} diff --git a/HOWTO.md b/HOWTO.md index d0c9b6ae..22542f54 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -125,6 +125,31 @@ curl -s -X POST http://localhost:8081/api/v1/auth/oidc/callback \ Local login remains available as fallback even when OIDC is enabled. +### Authentik Setup (OIDC Provider) + +Authentik is deployed alongside the Cameleer stack. After first deployment: + +1. **Initial setup**: Open `http://192.168.50.86:30900/if/flow/initial-setup/` and create the admin account +2. **Create provider**: Admin Interface → Providers → Create → OAuth2/OpenID Provider + - Name: `Cameleer` + - Authorization flow: `default-provider-authorization-explicit-consent` + - Client type: `Confidential` + - Redirect URIs: `http://192.168.50.86:30090/callback` (or your UI URL) + - Note the **Client ID** and **Client Secret** +3. **Create application**: Admin Interface → Applications → Create + - Name: `Cameleer` + - Provider: select `Cameleer` (created above) +4. **Configure roles** (optional): Create groups in Authentik and map them to Cameleer roles via the `roles-claim` config. Default claim path is `realm_access.roles`. For Authentik, you may need to customize the OIDC scope to include group claims. +5. **Set env vars** on the Cameleer server: + ``` + CAMELEER_OIDC_ENABLED=true + CAMELEER_OIDC_ISSUER=http://authentik:9000/application/o/cameleer/ + CAMELEER_OIDC_CLIENT_ID= + CAMELEER_OIDC_CLIENT_SECRET= + ``` + +For K8s deployment, these are managed via the `cameleer-oidc` secret (see CI/CD section). + ### User Management (ADMIN only) ```bash @@ -362,9 +387,13 @@ The full stack is deployed to k3s via CI/CD on push to `main`. K8s manifests are ``` cameleer namespace: - ClickHouse (StatefulSet, 2Gi PVC) ← clickhouse:8123 (ClusterIP) - cameleer3-server (Deployment) ← NodePort 30081 - cameleer3-ui (Deployment, Nginx) ← NodePort 30090 + ClickHouse (StatefulSet, 2Gi PVC) ← clickhouse:8123 (ClusterIP) + cameleer3-server (Deployment) ← NodePort 30081 + cameleer3-ui (Deployment, Nginx) ← NodePort 30090 + Authentik Server (Deployment) ← NodePort 30900 + Authentik Worker (Deployment) + Authentik PostgreSQL (StatefulSet, 1Gi) ← ClusterIP + Authentik Redis (Deployment) ← ClusterIP ``` ### Access (from your network) @@ -374,12 +403,13 @@ cameleer namespace: | Web UI | `http://192.168.50.86:30090` | | Server API | `http://192.168.50.86:30081/api/v1/health` | | Swagger UI | `http://192.168.50.86:30081/api/v1/swagger-ui.html` | +| Authentik | `http://192.168.50.86:30900` | ### CI/CD Pipeline Push to `main` triggers: **build** (UI npm + Maven, unit tests) → **docker** (buildx amd64 for server + UI, push to Gitea registry) → **deploy** (kubectl apply + rolling update). -Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_AUTH_TOKEN`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CAMELEER_UI_USER` (optional), `CAMELEER_UI_PASSWORD` (optional), `CAMELEER_JWT_SECRET` (recommended — tokens survive restarts). +Required Gitea org secrets: `REGISTRY_TOKEN`, `KUBECONFIG_BASE64`, `CAMELEER_AUTH_TOKEN`, `CAMELEER_JWT_SECRET`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`, `CAMELEER_UI_USER` (optional), `CAMELEER_UI_PASSWORD` (optional), `AUTHENTIK_PG_PASSWORD`, `AUTHENTIK_SECRET_KEY`, `CAMELEER_OIDC_ENABLED`, `CAMELEER_OIDC_ISSUER`, `CAMELEER_OIDC_CLIENT_ID`, `CAMELEER_OIDC_CLIENT_SECRET`. ### Manual K8s Commands diff --git a/deploy/authentik.yaml b/deploy/authentik.yaml new file mode 100644 index 00000000..a9f19e17 --- /dev/null +++ b/deploy/authentik.yaml @@ -0,0 +1,287 @@ +# Authentik OIDC Provider for Cameleer +# Provides external identity management with role-based access. +# +# After deployment: +# 1. Access Authentik at http://192.168.50.86:30900/if/flow/initial-setup/ +# 2. Create an admin account +# 3. Create an OAuth2/OIDC Provider + Application for Cameleer (see HOWTO.md) +# 4. Set CAMELEER_OIDC_* env vars on the server deployment + +# --- PostgreSQL for Authentik --- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: authentik-postgresql + namespace: cameleer +spec: + serviceName: authentik-postgresql + replicas: 1 + selector: + matchLabels: + app: authentik-postgresql + template: + metadata: + labels: + app: authentik-postgresql + spec: + containers: + - name: postgresql + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: authentik + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_PASSWORD + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + subPath: pgdata + resources: + requests: + memory: "128Mi" + cpu: "50m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + exec: + command: ["pg_isready", "-U", "authentik"] + initialDelaySeconds: 15 + periodSeconds: 10 + readinessProbe: + exec: + command: ["pg_isready", "-U", "authentik"] + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik-postgresql + namespace: cameleer +spec: + clusterIP: None + selector: + app: authentik-postgresql + ports: + - port: 5432 + targetPort: 5432 + +# --- Redis for Authentik --- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-redis + namespace: cameleer +spec: + replicas: 1 + selector: + matchLabels: + app: authentik-redis + template: + metadata: + labels: + app: authentik-redis + spec: + containers: + - name: redis + image: redis:7-alpine + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] + ports: + - containerPort: 6379 + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "64Mi" + cpu: "25m" + limits: + memory: "256Mi" + cpu: "250m" + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: data + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik-redis + namespace: cameleer +spec: + selector: + app: authentik-redis + ports: + - port: 6379 + targetPort: 6379 + +# --- Authentik Server --- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-server + namespace: cameleer +spec: + replicas: 1 + selector: + matchLabels: + app: authentik-server + template: + metadata: + labels: + app: authentik-server + spec: + containers: + - name: server + image: ghcr.io/goauthentik/server:2024.12 + args: ["server"] + ports: + - containerPort: 9000 + name: http + - containerPort: 9443 + name: https + env: + - name: AUTHENTIK_POSTGRESQL__HOST + value: authentik-postgresql + - name: AUTHENTIK_POSTGRESQL__NAME + value: authentik + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_USER + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_PASSWORD + - name: AUTHENTIK_REDIS__HOST + value: authentik-redis + - name: AUTHENTIK_SECRET_KEY + valueFrom: + secretKeyRef: + name: authentik-credentials + key: AUTHENTIK_SECRET_KEY + resources: + requests: + memory: "512Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /-/health/live/ + port: 9000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /-/health/ready/ + port: 9000 + initialDelaySeconds: 15 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik + namespace: cameleer +spec: + type: NodePort + selector: + app: authentik-server + ports: + - port: 9000 + targetPort: 9000 + nodePort: 30900 + name: http + - port: 9443 + targetPort: 9443 + nodePort: 30943 + name: https + +# --- Authentik Worker --- +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-worker + namespace: cameleer +spec: + replicas: 1 + selector: + matchLabels: + app: authentik-worker + template: + metadata: + labels: + app: authentik-worker + spec: + containers: + - name: worker + image: ghcr.io/goauthentik/server:2024.12 + args: ["worker"] + env: + - name: AUTHENTIK_POSTGRESQL__HOST + value: authentik-postgresql + - name: AUTHENTIK_POSTGRESQL__NAME + value: authentik + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_USER + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: authentik-credentials + key: PG_PASSWORD + - name: AUTHENTIK_REDIS__HOST + value: authentik-redis + - name: AUTHENTIK_SECRET_KEY + valueFrom: + secretKeyRef: + name: authentik-credentials + key: AUTHENTIK_SECRET_KEY + resources: + requests: + memory: "256Mi" + cpu: "50m" + limits: + memory: "512Mi" + cpu: "500m" diff --git a/deploy/server.yaml b/deploy/server.yaml index 419afee2..34d3e6ae 100644 --- a/deploy/server.yaml +++ b/deploy/server.yaml @@ -52,6 +52,36 @@ spec: optional: true - name: CAMELEER_UI_ORIGIN value: "http://192.168.50.86:30090" + - name: CAMELEER_JWT_SECRET + valueFrom: + secretKeyRef: + name: cameleer-auth + key: CAMELEER_JWT_SECRET + optional: true + - name: CAMELEER_OIDC_ENABLED + valueFrom: + secretKeyRef: + name: cameleer-oidc + key: CAMELEER_OIDC_ENABLED + optional: true + - name: CAMELEER_OIDC_ISSUER + valueFrom: + secretKeyRef: + name: cameleer-oidc + key: CAMELEER_OIDC_ISSUER + optional: true + - name: CAMELEER_OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: cameleer-oidc + key: CAMELEER_OIDC_CLIENT_ID + optional: true + - name: CAMELEER_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: cameleer-oidc + key: CAMELEER_OIDC_CLIENT_SECRET + optional: true resources: requests: memory: "256Mi"