From 2bc85826f15f95223e0dc4bfdadb2bcb794717c4 Mon Sep 17 00:00:00 2001 From: Adam Carr Date: Sat, 19 Sep 2026 21:56:58 -0700 Subject: [PATCH] Admin stack: Authelia SSO, portal at admin.carr.pub, gated grafana.carr.pub --- .gitignore | 3 + manifests/admin/authelia.yaml | 133 +++++++++++++++++++++++++++++++++ manifests/admin/ingresses.yaml | 61 +++++++++++++++ manifests/admin/portal.yaml | 66 ++++++++++++++++ scripts/deploy-admin.sh | 24 ++++++ 5 files changed, 287 insertions(+) create mode 100644 manifests/admin/authelia.yaml create mode 100644 manifests/admin/ingresses.yaml create mode 100644 manifests/admin/portal.yaml create mode 100644 scripts/deploy-admin.sh diff --git a/.gitignore b/.gitignore index b3ec217..d820081 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ configs/node-password.txt .synology-password +configs/admin-password.txt +configs/authelia-session-secret.txt +configs/authelia-jwt-secret.txt diff --git a/manifests/admin/authelia.yaml b/manifests/admin/authelia.yaml new file mode 100644 index 0000000..3d44155 --- /dev/null +++ b/manifests/admin/authelia.yaml @@ -0,0 +1,133 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: admin +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: authelia-config + namespace: admin +data: + configuration.yml: | + server: + address: tcp://:9091 + theme: dark + log: + level: info + authentication_backend: + file: + path: /config/users_database.yml + password_policy: + standard: + enabled: true + min_length: 12 + session: + cookies: + - domain: carr.pub + authelia_url: https://auth.carr.pub + default_redirection_url: https://admin.carr.pub + same_site: lax + storage: + local: + path: /config/data/db.sqlite3 + notifier: + filesystem: + filename: /config/data/notifications.txt + totp: + issuer: carr.pub + webauthn: + disable: true + access_control: + default_policy: one_factor + rules: [] +--- +apiVersion: v1 +kind: Secret +metadata: + name: authelia-users + namespace: admin +stringData: + users_database.yml: | + users: + adamcarr: + displayname: "Adam Carr" + password: "" + groups: ["admins"] +--- +apiVersion: v1 +kind: Secret +metadata: + name: authelia-secrets + namespace: admin +stringData: + session-secret: +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: authelia-data + namespace: admin +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: + requests: {storage: 2Gi} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authelia + namespace: admin +spec: + replicas: 1 + selector: + matchLabels: {app: authelia} + template: + metadata: + labels: {app: authelia} + spec: + containers: + - name: authelia + image: authelia/authelia:v4.39.28 + args: ["--config", "/config/configuration.yml"] + env: + - name: AUTHELIA_SESSION_SECRET + valueFrom: {secretKeyRef: {name: authelia-secrets, key: session-secret}} + ports: [{containerPort: 9091}] + resources: + requests: {cpu: 50m, memory: 64Mi} + limits: {memory: 256Mi} + volumeMounts: + - {name: config, mountPath: /config/configuration.yml, subPath: configuration.yml} + - {name: users, mountPath: /config/users_database.yml, subPath: users_database.yml} + - {name: data, mountPath: /config/data} + volumes: + - name: config + configMap: {name: authelia-config} + - name: users + secret: {secretName: authelia-users} + - name: data + persistentVolumeClaim: {claimName: authelia-data} +--- +apiVersion: v1 +kind: Service +metadata: + name: authelia + namespace: admin +spec: + selector: {app: authelia} + ports: + - port: 9091 + targetPort: 9091 +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: authelia-auth + namespace: admin +spec: + forwardAuth: + address: "http://authelia.admin.svc:9091/api/verify?rd=https://auth.carr.pub" + trustForwardHeader: true + authResponseHeaders: [Remote-User, Remote-Groups, Remote-Email, Remote-Name] diff --git a/manifests/admin/ingresses.yaml b/manifests/admin/ingresses.yaml new file mode 100644 index 0000000..83ccbc6 --- /dev/null +++ b/manifests/admin/ingresses.yaml @@ -0,0 +1,61 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: auth + namespace: admin + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + tls: + - hosts: [auth.carr.pub] + secretName: auth-carr-pub-tls + rules: + - host: auth.carr.pub + http: + paths: + - path: / + pathType: Prefix + backend: + service: {name: authelia, port: {number: 9091}} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: admin-portal + namespace: admin + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: admin-authelia-auth@kubernetescrd +spec: + tls: + - hosts: [admin.carr.pub] + secretName: admin-carr-pub-tls + rules: + - host: admin.carr.pub + http: + paths: + - path: / + pathType: Prefix + backend: + service: {name: portal, port: {number: 80}} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: grafana + namespace: monitoring + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: admin-authelia-auth@kubernetescrd +spec: + tls: + - hosts: [grafana.carr.pub] + secretName: grafana-carr-pub-tls + rules: + - host: grafana.carr.pub + http: + paths: + - path: / + pathType: Prefix + backend: + service: {name: grafana, port: {number: 3000}} diff --git a/manifests/admin/portal.yaml b/manifests/admin/portal.yaml new file mode 100644 index 0000000..ba96682 --- /dev/null +++ b/manifests/admin/portal.yaml @@ -0,0 +1,66 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: portal-index + namespace: admin +data: + index.html: | + + + planck admin + + +
+

planck

+ Grafana + Account & 2FA settings +

20 nodes · k3s · carr.pub

+
+ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: portal + namespace: admin +spec: + replicas: 2 + selector: + matchLabels: {app: portal} + template: + metadata: + labels: {app: portal} + spec: + containers: + - name: web + image: nginx:1.29-alpine + ports: [{containerPort: 80}] + resources: + requests: {cpu: 10m, memory: 16Mi} + limits: {memory: 64Mi} + volumeMounts: + - {name: html, mountPath: /usr/share/nginx/html} + volumes: + - name: html + configMap: {name: portal-index} +--- +apiVersion: v1 +kind: Service +metadata: + name: portal + namespace: admin +spec: + selector: {app: portal} + ports: + - port: 80 + targetPort: 80 diff --git a/scripts/deploy-admin.sh b/scripts/deploy-admin.sh new file mode 100644 index 0000000..d7aec49 --- /dev/null +++ b/scripts/deploy-admin.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Redeploy the admin stack (Authelia SSO + portal). Secrets are regenerated +# from the gitignored files in configs/ so nothing sensitive is committed. +# First-time setup: run scripts/gen-admin-secrets.sh before this. +set -euo pipefail +cd "$(dirname "$0")/.." + +HASH=$(/tmp/opencode/authelia/authelia crypto hash generate argon2 \ + --password "$(cat configs/admin-password.txt)" 2>/dev/null | grep -o '\$argon2id\$.*') +SESSION=$(cat configs/authelia-session-secret.txt) +JWT=$(cat configs/authelia-jwt-secret.txt) +ENCKEY=$(openssl rand -base64 24 | tr -d "=+/" | cut -c1-32) + +export HASH SESSION JWT ENCKEY +for f in manifests/admin/*.yaml; do + envsubst '\$HASH \$SESSION \$JWT \$ENCKEY' < "$f" | kubectl apply -f - +done + +kubectl -n admin set env deployment/authelia \ + AUTHELIA_SESSION_SECRET="$SESSION" \ + AUTHELIA_STORAGE_ENCRYPTION_KEY="$ENCKEY" \ + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET="$JWT" \ + >/dev/null +echo "admin stack deployed"