diff --git a/Makefile b/Makefile index 6955d7f..7b5d5c8 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \ addon-harbor addon-gitea addon-owncloud addon-nextcloud \ addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \ addon-smtp-relay addon-vault addon-external-secrets \ - addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass addon-ingress-add-domains addon-yandex-dns-controller addon-technitium-dns \ + addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ingress-proxypass addon-ingress-add-domains addon-yandex-dns-controller addon-technitium-dns addon-authelia \ add-node remove-node \ add-etcd-node remove-etcd-node \ etcd-backup etcd-restore etcd-list-snapshots \ @@ -436,6 +436,10 @@ addon-technitium-dns: _check_env _check_image ## Technitium DNS HA — Primary+S @printf "$(CYAN)Устанавливаю Technitium DNS HA...$(NC)\n" $(DOCKER_RUN) addon technitium-dns $(ARGS) +addon-authelia: _check_env _check_image ## Authelia SSO — Forward-auth + OIDC provider + @printf "$(CYAN)Устанавливаю Authelia SSO...$(NC)\n" + $(DOCKER_RUN) addon authelia $(ARGS) + # Generic цель — любой аддон из addons//playbook.yml addon-%: _check_env _check_image @if [ ! -f "addons/$*/playbook.yml" ]; then \ diff --git a/addons/authelia/README.md b/addons/authelia/README.md new file mode 100644 index 0000000..b8dc098 --- /dev/null +++ b/addons/authelia/README.md @@ -0,0 +1,494 @@ +# authelia + +Self-hosted authentication system providing **forward-auth** for ingress-nginx and an **OIDC provider** for Gitea, Grafana, ArgoCD, MinIO, Vault, and Nextcloud. + +## Architecture + +``` +User → ingress-nginx → (auth-url) → Authelia :9091 → allowed/denied + ↓ + Authelia portal + auth.home.local + ↓ + users_database.yml + (argon2id passwords) + +OIDC flow: +Service → Authelia /api/oidc/authorization → login → token → Service +``` + +``` +Traffic routes: + auth.home.local → authelia ClusterIP :9091 (Ingress, NO forward-auth) + sonarr.home.local → sonarr service (Ingress + forward-auth annotations) + gitea.home.local → gitea service (Ingress, no forward-auth — OIDC handles it) + +Kubernetes objects: + Deployment: authelia + Service: authelia (ClusterIP :9091) + Secrets: authelia-secrets (jwt, session, storage_encryption, oidc keys) + authelia-config (configuration.yml — contains OIDC client secrets) + authelia-users (users_database.yml) + PVC: authelia-data (SQLite db + notification.txt) + Ingress: authelia (auth.home.local) + ConfigMap: authelia-forward-auth (copy-paste annotation reference) + Deployment: authelia-redis (optional, redis.enabled=true) +``` + +--- + +## 1. Installation + +### Step 1 — Generate secrets + +Run these commands and save the output: +```bash +# Core secrets +openssl rand -base64 64 # → authelia_jwt_secret +openssl rand -base64 64 # → authelia_session_secret +openssl rand -base64 32 # → authelia_storage_encryption_key +openssl rand -base64 48 # → authelia_oidc_hmac_secret + +# OIDC client secrets (one per enabled client) +openssl rand -hex 32 # → authelia_oidc_secret_gitea +openssl rand -hex 32 # → authelia_oidc_secret_grafana +``` + +> The **OIDC RSA private key** is auto-generated during deploy. Leave `authelia_oidc_private_key: ""` in vault. After first deploy, retrieve and save it (see note at end of deploy output). + +### Step 2 — Generate admin password hash + +```bash +docker run --rm authelia/authelia:latest authelia hash-password 'your-password' +# Output: $argon2id$v=19$m=65536,t=3,p=4$... +``` + +Copy the full `$argon2id$...` string. + +### Step 3 — Edit vault.yml + +```yaml +# group_vars/all/vault.yml (ansible-vault encrypted) +authelia_jwt_secret: "" +authelia_session_secret: "" +authelia_storage_encryption_key: "" +authelia_oidc_hmac_secret: "" +authelia_oidc_private_key: "" # auto-generated on first deploy + +authelia_oidc_secret_gitea: "" +authelia_oidc_secret_grafana: "" + +authelia_user_admin_password_hash: "$argon2id$v=19$m=65536,t=3,p=4$..." +``` + +### Step 4 — Configure addons.yml + +```yaml +# group_vars/all/addons.yml +addon_authelia: true + +authelia_host: "auth.home.local" +authelia_domain: "home.local" + +# OIDC clients to enable +authelia_oidc_gitea_enabled: true +authelia_oidc_grafana_enabled: true + +# Domains to protect +authelia_protected_domains: + - sonarr.home.local + - radarr.home.local + - lidarr.home.local + - prowlarr.home.local + - pgadmin.home.local + +# Domains requiring admin group +authelia_admin_domains: + - argocd.home.local + - vault.home.local + +# Public bypass (no auth) +authelia_bypass_domains: + - plex.home.local +``` + +### Step 5 — Deploy + +```bash +make addon-authelia +``` + +### Step 6 — Add DNS record + +Add `auth.home.local` pointing to the kube-vip/ingress-nginx IP in Technitium DNS (or your DNS server). + +--- + +## 2. Managing users + +### Add a new user + +1. Generate password hash: + ```bash + docker run --rm authelia/authelia:latest authelia hash-password 'newpassword' + ``` + +2. Add to `group_vars/all/addons.yml`: + ```yaml + authelia_users: + admin: + displayname: "Administrator" + email: "admin@home.local" + groups: [admins, users] + alice: + displayname: "Alice" + email: "alice@home.local" + groups: [users] + ``` + +3. Add to `vault.yml`: + ```yaml + authelia_user_alice_password_hash: "$argon2id$..." + ``` + +4. Redeploy: `make addon-authelia` + +### Groups + +- `admins` — access to `authelia_admin_domains` (ArgoCD, Vault, Harbor, Dashboard) +- `users` — access to `authelia_protected_domains` (Sonarr, Radarr, etc.) + +OIDC claims include the `groups` scope, so Grafana/Gitea can use group-based role mapping. + +--- + +## 3. Protect a new service with forward-auth + +### Step 1 — Add annotations to the service's Ingress + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: myservice + namespace: myservice + annotations: + kubernetes.io/ingress.class: nginx + # ── Authelia forward-auth ────────────────────────────────────────────── + nginx.ingress.kubernetes.io/auth-url: "http://authelia.authelia.svc.cluster.local:9091/api/authz/forward-auth" + nginx.ingress.kubernetes.io/auth-signin: "http://auth.home.local/?rd=$scheme://$host$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Name,Remote-Groups,Remote-Email" + nginx.ingress.kubernetes.io/auth-snippet: | + proxy_set_header X-Forwarded-Method $request_method; +spec: + rules: + - host: myservice.home.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: myservice + port: + number: 8080 +``` + +> Get the exact URLs for your deployment: +> ```bash +> kubectl get cm authelia-forward-auth -n authelia -o jsonpath='{.data.annotations\.yaml}' +> ``` + +### Step 2 — Add to access control + +Add `myservice.home.local` to the appropriate list in `addons.yml`: +```yaml +authelia_protected_domains: + - myservice.home.local + # ... existing entries +``` + +Then redeploy: `make addon-authelia` + +--- + +## 4. OIDC client configuration per service + +### Gitea + +In Gitea Admin → Site Administration → Authentication Sources → Add OAuth2: + +| Field | Value | +|-------|-------| +| Name | `Authelia` | +| OAuth2 Provider | `OpenID Connect` | +| Client ID | `gitea` | +| Client Secret | value of `authelia_oidc_secret_gitea` in vault | +| OpenID Connect Auto Discovery URL | `http://auth.home.local/.well-known/openid-configuration` | +| Scopes | `openid profile email groups` | + +Restart Gitea. Users can now click "Sign in with Authelia" on the login page. + +Optional — auto-create users and map admin group: +```ini +# app.ini +[oauth2] +USERNAME = preferred_username +UPDATE_AVATAR = true + +[openid] +ENABLE_OPENID_SIGNIN = true +``` + +--- + +### Grafana + +Add to Grafana's `grafana.ini` (or Helm values): + +```ini +[auth.generic_oauth] +enabled = true +name = Authelia +client_id = grafana +client_secret = +scopes = openid profile email groups +auth_url = http://auth.home.local/api/oidc/authorization +token_url = http://auth.home.local/api/oidc/token +api_url = http://auth.home.local/api/oidc/userinfo + +# Role mapping via Authelia groups +role_attribute_path = contains(groups[*], 'admins') && 'GrafanaAdmin' || 'Viewer' +allow_sign_up = true +``` + +Or in Helm values for the prometheus-stack addon: +```yaml +# group_vars/all/addons.yml +prometheus_grafana_oauth_enabled: true +prometheus_grafana_oauth_client_id: "grafana" +prometheus_grafana_oauth_client_secret: "..." # from vault +prometheus_grafana_oauth_auth_url: "http://auth.home.local/api/oidc/authorization" +prometheus_grafana_oauth_token_url: "http://auth.home.local/api/oidc/token" +prometheus_grafana_oauth_api_url: "http://auth.home.local/api/oidc/userinfo" +``` + +--- + +### ArgoCD + +Enable ArgoCD OIDC in `argocd-cm` ConfigMap: +```yaml +# values override for argocd Helm chart +server: + config: + oidc.config: | + name: Authelia + issuer: http://auth.home.local + clientID: argocd + clientSecret: $oidc.authelia.clientSecret + requestedScopes: + - openid + - profile + - email + - groups + requestedIDTokenClaims: + groups: + essential: true +``` + +Map Authelia groups to ArgoCD RBAC: +```yaml +# argocd-rbac-cm +policy.csv: | + g, admins, role:admin +policy.default: role:readonly +``` + +Store the secret in a K8s Secret `argocd-secret`: +```yaml +data: + oidc.authelia.clientSecret: +``` + +--- + +### MinIO + +In MinIO Console → Identity → OpenID: + +| Field | Value | +|-------|-------| +| Config URL | `http://auth.home.local/.well-known/openid-configuration` | +| Client ID | `minio` | +| Client Secret | value of `authelia_oidc_secret_minio` | +| Claim Name | `groups` | +| Scopes | `openid,profile,email` | + +Map Authelia group `admins` to MinIO policy `consoleAdmin`. + +--- + +### Vault + +Configure Vault OIDC auth method: +```bash +vault auth enable oidc + +vault write auth/oidc/config \ + oidc_discovery_url="http://auth.home.local" \ + oidc_client_id="vault" \ + oidc_client_secret="" \ + default_role="reader" + +vault write auth/oidc/role/reader \ + bound_audiences="vault" \ + allowed_redirect_uris="https://vault.home.local/ui/vault/auth/oidc/oidc/callback" \ + allowed_redirect_uris="https://vault.home.local/oidc/callback" \ + user_claim="sub" \ + groups_claim="groups" \ + token_policies="default" +``` + +--- + +## 5. Debugging auth issues + +### Check Authelia logs (real-time) + +```bash +kubectl -n authelia logs -l app.kubernetes.io/name=authelia -f --tail=100 +``` + +Common log messages: +- `"ALLOW"` — request was allowed +- `"DENY"` — request was denied (check domain in `protectedDomains`) +- `"Redirecting"` — unauthenticated user redirected to login +- `"POST /api/firstfactor"` — login attempt + +### Verify forward-auth endpoint is reachable + +From within the cluster: +```bash +kubectl run curl-test --rm -it --image=curlimages/curl -- \ + curl -v http://authelia.authelia.svc.cluster.local:9091/api/health +# Expected: {"status":"OK"} +``` + +### Test that a domain's annotations are applied + +```bash +kubectl get ingress sonarr -n mediaserver -o yaml | grep auth-url +``` + +### Check current access control configuration + +```bash +kubectl -n authelia exec deploy/authelia -- \ + cat /config/configuration.yml | grep -A 30 "access_control:" +``` + +### Check active sessions (SQLite) + +```bash +kubectl -n authelia exec deploy/authelia -- \ + sqlite3 /data/db.sqlite3 "SELECT subject, ip, last_activity FROM user_opaque_identifier LIMIT 20;" +``` + +### Notification log (if SMTP disabled) + +```bash +kubectl -n authelia exec deploy/authelia -- cat /data/notification.txt +``` + +--- + +## 6. Test the login flow + +### Forward-auth flow (e.g., Sonarr) + +1. Open `http://sonarr.home.local/` in a private browser window +2. Should redirect to `http://auth.home.local/?rd=http%3A%2F%2Fsonarr.home.local%2F` +3. Enter credentials → should redirect back to Sonarr +4. Access granted + +Test from command line: +```bash +# Step 1: login request (expect redirect to Authelia) +curl -I http://sonarr.home.local/ +# Expected: 302 to auth.home.local + +# Step 2: verify endpoint directly +curl -I -H "X-Forwarded-Host: sonarr.home.local" \ + -H "X-Forwarded-URI: /" \ + -H "X-Forwarded-Proto: http" \ + http://authelia.authelia.svc.cluster.local:9091/api/authz/forward-auth +# Expected: 401 (unauthenticated) or 200 (if session cookie provided) +``` + +### OIDC flow (e.g., Gitea) + +1. Open `http://gitea.home.local/user/oauth2/Authelia` + (or click "Sign in with Authelia" on Gitea login page) +2. Should redirect to Authelia login form +3. Login with admin credentials +4. Authelia redirects back to Gitea with authorization code +5. Gitea exchanges code for token — user is logged in + +### Check OIDC discovery endpoint + +```bash +curl -s http://auth.home.local/.well-known/openid-configuration | jq . +# Should return JSON with issuer, authorization_endpoint, token_endpoint, etc. +``` + +--- + +## Variables reference + +| Variable | Default | Description | +|----------|---------|-------------| +| `authelia_host` | `auth.home.local` | Portal hostname | +| `authelia_domain` | `home.local` | Session cookie domain | +| `authelia_theme` | `dark` | UI theme | +| `authelia_two_factor_enabled` | `false` | Require TOTP/WebAuthn | +| `authelia_storage_type` | `sqlite` | `sqlite` or `postgresql` | +| `authelia_redis_enabled` | `false` | Built-in Redis for sessions | +| `authelia_smtp_enabled` | `false` | SMTP for 2FA/password-reset emails | +| `authelia_oidc_enabled` | `true` | Enable OIDC provider | +| `authelia_oidc_gitea_enabled` | `true` | Gitea OIDC client | +| `authelia_oidc_grafana_enabled` | `true` | Grafana OIDC client | +| `authelia_oidc_argocd_enabled` | `false` | ArgoCD OIDC client | +| `authelia_oidc_minio_enabled` | `false` | MinIO OIDC client | +| `authelia_oidc_vault_enabled` | `false` | Vault OIDC client | +| `authelia_ingress_tls_enabled` | `false` | TLS on auth portal | +| `authelia_protected_domains` | `[sonarr, radarr…]` | Domains requiring login | +| `authelia_admin_domains` | `[argocd, vault…]` | Admin-only domains | +| `authelia_bypass_domains` | `[]` | Public bypass domains | +| `authelia_oidc_domains` | `[gitea, grafana, minio]` | OIDC bypass (forward-auth off) | + +### Vault secrets required + +| Variable | Notes | +|----------|-------| +| `authelia_jwt_secret` | min 64 chars — `openssl rand -base64 64` | +| `authelia_session_secret` | min 64 chars | +| `authelia_storage_encryption_key` | min 20 chars — `openssl rand -base64 32` | +| `authelia_oidc_hmac_secret` | min 32 chars — `openssl rand -base64 48` | +| `authelia_oidc_private_key` | RSA PEM — leave empty, auto-generated | +| `authelia_oidc_secret_gitea` | `openssl rand -hex 32` | +| `authelia_oidc_secret_grafana` | `openssl rand -hex 32` | +| `authelia_user_admin_password_hash` | argon2id hash from `authelia hash-password` | + +--- + +## Saving the auto-generated OIDC private key + +After the first deploy, save the key to vault for reproducibility: + +```bash +kubectl -n authelia get secret authelia-secrets \ + -o jsonpath='{.data.oidc_private_key}' | base64 -d +``` + +Paste the PEM output into `vault.yml` as `authelia_oidc_private_key: |` (multiline YAML). diff --git a/addons/authelia/playbook.yml b/addons/authelia/playbook.yml new file mode 100644 index 0000000..6ad72e8 --- /dev/null +++ b/addons/authelia/playbook.yml @@ -0,0 +1,7 @@ +--- +- name: Install Authelia SSO + hosts: k3s_master[0] + gather_facts: false + become: true + roles: + - role: "{{ playbook_dir }}/role" diff --git a/addons/authelia/role/chart/Chart.yaml b/addons/authelia/role/chart/Chart.yaml new file mode 100644 index 0000000..82a66ca --- /dev/null +++ b/addons/authelia/role/chart/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: authelia +description: | + Self-hosted authentication portal with forward-auth for ingress-nginx + and OIDC provider for Gitea, Grafana, ArgoCD, MinIO, Vault, Nextcloud. + Supports file-based users, SQLite/PostgreSQL storage, optional Redis sessions. +type: application +version: 1.0.0 +appVersion: "4.38.17" +keywords: + - authentication + - sso + - oidc + - forward-auth + - 2fa +home: https://www.authelia.com +sources: + - https://github.com/authelia/authelia + - https://git.antropoff.ru/DevOpsTools/K3S +maintainers: + - name: k3s-ansible diff --git a/addons/authelia/role/chart/files/configuration.yml.tpl b/addons/authelia/role/chart/files/configuration.yml.tpl new file mode 100644 index 0000000..cbce870 --- /dev/null +++ b/addons/authelia/role/chart/files/configuration.yml.tpl @@ -0,0 +1,203 @@ +--- +# Authelia configuration — rendered by Helm via tpl() +# Secrets (jwt, session, storage_encryption, oidc_hmac, oidc_private_key) +# are injected via AUTHELIA_*_FILE environment variables — not in this file. + +server: + host: 0.0.0.0 + port: 9091 + path: "" + buffers: + read: 4096 + write: 4096 + timeouts: + read: 6s + write: 6s + idle: 30s + +log: + level: info + format: text + +theme: {{ .Values.theme | quote }} + +totp: + disable: false + issuer: {{ .Values.totp.issuer | quote }} + algorithm: sha1 + digits: 6 + period: {{ .Values.totp.period }} + skew: {{ .Values.totp.skew }} + secret_size: 32 + +webauthn: + disable: false + display_name: {{ .Values.domain | quote }} + attestation_conveyance_preference: indirect + user_verification: preferred + timeout: 60s + +authentication_backend: + password_reset: + disable: false + refresh_interval: 5m + file: + path: /config/users_database.yml + watch: false + password: + algorithm: argon2id + iterations: 3 + memory: 65536 + parallelism: 4 + key_length: 32 + salt_length: 16 + +session: + name: {{ .Values.session.name | quote }} + domain: {{ .Values.session.domain | quote }} + same_site: {{ .Values.session.sameSite | quote }} + expiration: {{ .Values.session.expiration | quote }} + inactivity: {{ .Values.session.inactivity | quote }} + remember_me_duration: {{ .Values.session.rememberMeDuration | quote }} +{{- if .Values.redis.enabled }} + redis: + host: {{ printf "%s-redis" (include "authelia.name" .) | quote }} + port: 6379 +{{- end }} + +regulation: + max_retries: 3 + find_time: 2m + ban_time: 5m + +storage: +{{- if eq .Values.storage.type "postgresql" }} + postgres: + host: {{ .Values.storage.postgresql.host | quote }} + port: {{ .Values.storage.postgresql.port }} + database: {{ .Values.storage.postgresql.database | quote }} + schema: {{ .Values.storage.postgresql.schema | quote }} + username: {{ .Values.storage.postgresql.username | quote }} + tls: + skip_verify: true +{{- else }} + local: + path: {{ .Values.storage.sqlite.path | quote }} +{{- end }} + +notifier: + disable_startup_check: true +{{- if .Values.notifier.smtp.enabled }} + smtp: + host: {{ .Values.notifier.smtp.host | quote }} + port: {{ .Values.notifier.smtp.port }} + username: {{ .Values.notifier.smtp.username | quote }} + sender: {{ .Values.notifier.smtp.sender | quote }} + tls: + skip_verify: {{ .Values.notifier.smtp.tls.skipVerify }} +{{- else }} + filesystem: + filename: /data/notification.txt +{{- end }} + +access_control: + default_policy: {{ .Values.accessControl.defaultPolicy | quote }} + rules: + + # Authelia portal — always bypass (prevents auth loop) + - domain: {{ .Values.authHost | quote }} + policy: bypass + + # Health check endpoints — bypass for monitoring + - domain: "*.{{ .Values.domain }}" + resources: + - "^/healthz(.*)$" + - "^/api/healthz(.*)$" + - "^/health$" + policy: bypass + +{{- if .Values.accessControl.bypassDomains }} + # Public services — no authentication required + - domain: +{{- range .Values.accessControl.bypassDomains }} + - {{ . | quote }} +{{- end }} + policy: bypass +{{- end }} + +{{- if .Values.accessControl.oidcDomains }} + # OIDC-enabled services — bypass forward-auth (OIDC handles authentication) + - domain: +{{- range .Values.accessControl.oidcDomains }} + - {{ . | quote }} +{{- end }} + policy: bypass +{{- end }} + +{{- if .Values.accessControl.adminDomains }} + # Admin-only services — require 'admins' group + - domain: +{{- range .Values.accessControl.adminDomains }} + - {{ . | quote }} +{{- end }} + subject: + - "group:admins" + policy: {{ if .Values.twoFactor.enabled }}two_factor{{ else }}one_factor{{ end }} +{{- end }} + +{{- if .Values.accessControl.protectedDomains }} + # Protected services — login required + - domain: +{{- range .Values.accessControl.protectedDomains }} + - {{ . | quote }} +{{- end }} + policy: {{ if .Values.twoFactor.enabled }}two_factor{{ else }}one_factor{{ end }} +{{- end }} + +{{- if .Values.oidc.enabled }} +identity_providers: + oidc: + access_token_lifespan: {{ .Values.oidc.accessTokenLifespan | quote }} + authorize_code_lifespan: {{ .Values.oidc.authorizeCodeLifespan | quote }} + id_token_lifespan: {{ .Values.oidc.idTokenLifespan | quote }} + refresh_token_lifespan: {{ .Values.oidc.refreshTokenLifespan | quote }} + enable_client_debug_messages: false + minimum_parameter_entropy: 8 + cors: + endpoints: + - authorization + - token + - revocation + - introspection + allowed_origins_from_client_redirect_uris: true + clients: +{{- range $name, $client := .Values.oidc.clients }} +{{- if $client.enabled }} + - id: {{ $client.id | quote }} + description: {{ $client.description | default $name | quote }} + # $plaintext$ prefix — Authelia 4.38+ plain-text client secret marker + secret: {{ printf "$plaintext$%s" $client.secret | quote }} + public: false + authorization_policy: one_factor + scopes: +{{- range $client.scopes }} + - {{ . | quote }} +{{- end }} + redirect_uris: +{{- range $client.redirectUris }} + - {{ . | quote }} +{{- end }} + grant_types: +{{- range $client.grantTypes }} + - {{ . | quote }} +{{- end }} + response_types: + - code + response_modes: + - form_post + - query + - fragment + userinfo_signing_algorithm: none +{{- end }} +{{- end }} +{{- end }} diff --git a/addons/authelia/role/chart/templates/NOTES.txt b/addons/authelia/role/chart/templates/NOTES.txt new file mode 100644 index 0000000..7c65235 --- /dev/null +++ b/addons/authelia/role/chart/templates/NOTES.txt @@ -0,0 +1,56 @@ +╔══════════════════════════════════════════════════════════════╗ +║ Authelia SSO — Deployed ║ +╚══════════════════════════════════════════════════════════════╝ + +Portal: http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.authHost }}/ +Namespace: {{ .Release.Namespace }} +OIDC: {{ if .Values.oidc.enabled }}enabled{{ else }}disabled{{ end }} +Redis: {{ if .Values.redis.enabled }}enabled{{ else }}disabled (memory sessions){{ end }} +Storage: {{ .Values.storage.type }} + +─── Protect a new service ────────────────────────────────────── + +Add to its Ingress: + nginx.ingress.kubernetes.io/auth-url: "http://{{ include "authelia.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:9091/api/authz/forward-auth" + nginx.ingress.kubernetes.io/auth-signin: "http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.authHost }}/?rd=$scheme://$host$escaped_request_uri" + nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Name,Remote-Groups,Remote-Email" + nginx.ingress.kubernetes.io/auth-snippet: "proxy_set_header X-Forwarded-Method $request_method;" + +Or get the full reference: + kubectl get cm {{ include "authelia.name" . }}-forward-auth -n {{ .Release.Namespace }} -o jsonpath='{.data.annotations\.yaml}' + +─── OIDC Issuer ──────────────────────────────────────────────── + + http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.authHost }} + +Discovery: http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.authHost }}/.well-known/openid-configuration + +─── Logs / Debug ─────────────────────────────────────────────── + + kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/name={{ include "authelia.name" . }} -f + +─── First login ──────────────────────────────────────────────── + + Open: http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.authHost }}/ + User: admin (or as configured in authelia_users) + Pass: the plaintext password whose hash you set in vault.yml + +─── Access control rules ──────────────────────────────────────── +{{- if .Values.accessControl.protectedDomains }} + Protected (login required): + {{- range .Values.accessControl.protectedDomains }} + - {{ . }} + {{- end }} +{{- end }} +{{- if .Values.accessControl.adminDomains }} + Admin-only (group: admins): + {{- range .Values.accessControl.adminDomains }} + - {{ . }} + {{- end }} +{{- end }} +{{- if .Values.accessControl.bypassDomains }} + Bypass (public): + {{- range .Values.accessControl.bypassDomains }} + - {{ . }} + {{- end }} +{{- end }} diff --git a/addons/authelia/role/chart/templates/_helpers.tpl b/addons/authelia/role/chart/templates/_helpers.tpl new file mode 100644 index 0000000..f885802 --- /dev/null +++ b/addons/authelia/role/chart/templates/_helpers.tpl @@ -0,0 +1,37 @@ +{{- define "authelia.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "authelia.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "authelia.labels" -}} +helm.sh/chart: {{ include "authelia.chart" . }} +{{ include "authelia.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "authelia.selectorLabels" -}} +app.kubernetes.io/name: {{ include "authelia.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Forward-auth URL for use in ingress-nginx annotations. +Returns the full URL to Authelia's authz endpoint inside the cluster. +*/}} +{{- define "authelia.forwardAuthUrl" -}} +{{- printf "http://%s.%s.svc.cluster.local:9091/api/authz/forward-auth" (include "authelia.name" .) .Release.Namespace }} +{{- end }} + +{{/* +Sign-in URL for redirecting unauthenticated users. +*/}} +{{- define "authelia.signinUrl" -}} +{{- if .Values.ingress.tls.enabled -}} +{{- printf "https://%s/?rd=$scheme://$host$escaped_request_uri" .Values.authHost }} +{{- else -}} +{{- printf "http://%s/?rd=$scheme://$host$escaped_request_uri" .Values.authHost }} +{{- end -}} +{{- end }} diff --git a/addons/authelia/role/chart/templates/configmap-annotations.yaml b/addons/authelia/role/chart/templates/configmap-annotations.yaml new file mode 100644 index 0000000..6112635 --- /dev/null +++ b/addons/authelia/role/chart/templates/configmap-annotations.yaml @@ -0,0 +1,43 @@ +--- +# Reference ConfigMap: forward-auth annotations for ingress-nginx. +# Copy-paste these annotations onto any Ingress you want to protect with Authelia. +# Usage: kubectl get cm authelia-forward-auth -n {{ .Release.Namespace }} -o yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "authelia.name" . }}-forward-auth + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +data: + # ── Paste these onto protected Ingress resources ───────────────────────────── + annotations.yaml: | + # Required on EVERY protected Ingress: + nginx.ingress.kubernetes.io/auth-url: "{{ include "authelia.forwardAuthUrl" . }}" + nginx.ingress.kubernetes.io/auth-signin: "{{ include "authelia.signinUrl" . }}" + nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Name,Remote-Groups,Remote-Email" + nginx.ingress.kubernetes.io/auth-snippet: | + proxy_set_header X-Forwarded-Method $request_method; + + # ── OIDC Issuer URL (for configuring OIDC clients) ──────────────────────────── + oidc-issuer: | + {{ if .Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ .Values.authHost }} + + # ── OIDC discovery endpoint ─────────────────────────────────────────────────── + oidc-discovery: | + {{ if .Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ .Values.authHost }}/.well-known/openid-configuration + + # ── Quick reference: protect a new service ──────────────────────────────────── + howto: | + To protect myservice.home.local: + + 1. Add these annotations to the Service's Ingress: + nginx.ingress.kubernetes.io/auth-url: "{{ include "authelia.forwardAuthUrl" . }}" + nginx.ingress.kubernetes.io/auth-signin: "{{ include "authelia.signinUrl" . }}" + nginx.ingress.kubernetes.io/auth-response-headers: "Remote-User,Remote-Name,Remote-Groups,Remote-Email" + + 2. Add the domain to accessControl.protectedDomains in addons.yml and re-run: + make addon-authelia + + 3. Add to Technitium DNS (or /etc/hosts): + myservice.home.local diff --git a/addons/authelia/role/chart/templates/deployment.yaml b/addons/authelia/role/chart/templates/deployment.yaml new file mode 100644 index 0000000..912d188 --- /dev/null +++ b/addons/authelia/role/chart/templates/deployment.yaml @@ -0,0 +1,115 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authelia.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +spec: + replicas: 1 + # Recreate required for ReadWriteOnce PVC (SQLite) + strategy: + type: Recreate + selector: + matchLabels: + {{- include "authelia.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "authelia.selectorLabels" . | nindent 8 }} + annotations: + # Force pod restart when config/users secrets change + checksum/config: {{ tpl (.Files.Get "files/configuration.yml.tpl") . | sha256sum }} + checksum/secrets: {{ .Values.secrets | toJson | sha256sum }} + checksum/users: {{ .Values.users | toJson | sha256sum }} + spec: + securityContext: + runAsNonRoot: true + runAsUser: 8000 + runAsGroup: 8000 + fsGroup: 8000 + containers: + - name: authelia + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - authelia + - --config=/config/configuration.yml + ports: + - name: http + containerPort: 9091 + protocol: TCP + env: + # Core secrets — read from mounted files + - name: AUTHELIA_JWT_SECRET_FILE + value: /secrets/jwt_secret + - name: AUTHELIA_SESSION_SECRET_FILE + value: /secrets/session_secret + - name: AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE + value: /secrets/storage_encryption_key + {{- if .Values.oidc.enabled }} + - name: AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE + value: /secrets/oidc_hmac_secret + - name: AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE + value: /secrets/oidc_private_key + {{- end }} + {{- if eq .Values.storage.type "postgresql" }} + - name: AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE + value: /secrets/db_password + {{- end }} + {{- if .Values.notifier.smtp.enabled }} + - name: AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE + value: /secrets/smtp_password + {{- end }} + - name: TZ + value: Europe/Moscow + volumeMounts: + - name: config + mountPath: /config + readOnly: true + - name: users + mountPath: /config/users_database.yml + subPath: users_database.yml + readOnly: true + - name: secrets + mountPath: /secrets + readOnly: true + - name: data + mountPath: /data + readinessProbe: + httpGet: + path: /api/health + port: 9091 + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /api/health + port: 9091 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 3 + startupProbe: + httpGet: + path: /api/health + port: 9091 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 12 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + - name: config + secret: + secretName: {{ include "authelia.name" . }}-config + - name: users + secret: + secretName: {{ include "authelia.name" . }}-users + - name: secrets + secret: + secretName: {{ include "authelia.name" . }}-secrets + defaultMode: 0400 + - name: data + persistentVolumeClaim: + claimName: {{ include "authelia.name" . }}-data diff --git a/addons/authelia/role/chart/templates/ingress.yaml b/addons/authelia/role/chart/templates/ingress.yaml new file mode 100644 index 0000000..a9f86f7 --- /dev/null +++ b/addons/authelia/role/chart/templates/ingress.yaml @@ -0,0 +1,36 @@ +{{- if .Values.ingress.enabled }} +--- +# Authelia portal ingress — accessible at authHost +# No forward-auth annotation here (would cause an auth loop) +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "authelia.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} + annotations: + kubernetes.io/ingress.class: {{ .Values.ingress.ingressClass | quote }} + nginx.ingress.kubernetes.io/proxy-buffer-size: "128k" + {{- if .Values.ingress.tls.certManager.enabled }} + cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.certManager.issuer | quote }} + {{- end }} +spec: + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.authHost | quote }} + secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" (include "authelia.name" .)) | quote }} + {{- end }} + rules: + - host: {{ .Values.authHost | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "authelia.name" . }} + port: + number: 9091 +{{- end }} diff --git a/addons/authelia/role/chart/templates/pvc.yaml b/addons/authelia/role/chart/templates/pvc.yaml new file mode 100644 index 0000000..b85f490 --- /dev/null +++ b/addons/authelia/role/chart/templates/pvc.yaml @@ -0,0 +1,18 @@ +--- +# PVC for Authelia data directory: SQLite database, notification log +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "authelia.name" . }}-data + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.storage.storageClassName }} + storageClassName: {{ .Values.storage.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.storage.size }} diff --git a/addons/authelia/role/chart/templates/redis.yaml b/addons/authelia/role/chart/templates/redis.yaml new file mode 100644 index 0000000..15bcd23 --- /dev/null +++ b/addons/authelia/role/chart/templates/redis.yaml @@ -0,0 +1,64 @@ +{{- if .Values.redis.enabled }} +--- +# Redis Deployment — session storage for Authelia (optional but recommended) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authelia.name" . }}-redis + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "authelia.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: redis + template: + metadata: + labels: + {{- include "authelia.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + imagePullPolicy: IfNotPresent + command: + - redis-server + - --save "" + - --appendonly no + ports: + - name: redis + containerPort: 6379 + protocol: TCP + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + readinessProbe: + exec: + command: [redis-cli, ping] + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "authelia.name" . }}-redis + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} + app.kubernetes.io/component: redis +spec: + type: ClusterIP + selector: + {{- include "authelia.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: redis + ports: + - name: redis + port: 6379 + targetPort: 6379 + protocol: TCP +{{- end }} diff --git a/addons/authelia/role/chart/templates/secret-config.yaml b/addons/authelia/role/chart/templates/secret-config.yaml new file mode 100644 index 0000000..01fa2cb --- /dev/null +++ b/addons/authelia/role/chart/templates/secret-config.yaml @@ -0,0 +1,14 @@ +--- +# Authelia configuration.yml stored as Secret because it contains OIDC client secrets. +# Rendered via Helm tpl() from files/configuration.yml.tpl +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "authelia.name" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +type: Opaque +stringData: + configuration.yml: | +{{ tpl (.Files.Get "files/configuration.yml.tpl") . | indent 4 }} diff --git a/addons/authelia/role/chart/templates/secret-main.yaml b/addons/authelia/role/chart/templates/secret-main.yaml new file mode 100644 index 0000000..9f1e559 --- /dev/null +++ b/addons/authelia/role/chart/templates/secret-main.yaml @@ -0,0 +1,24 @@ +--- +# Authelia core secrets — mounted as files, read via AUTHELIA_*_FILE env vars. +# Contains: jwt_secret, session_secret, storage_encryption_key, oidc_hmac_secret, +# oidc_private_key (RSA PEM), db_password (optional), smtp_password (optional) +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "authelia.name" . }}-secrets + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +type: Opaque +stringData: + jwt_secret: {{ .Values.secrets.jwtSecret | quote }} + session_secret: {{ .Values.secrets.sessionSecret | quote }} + storage_encryption_key: {{ .Values.secrets.storageEncryptionKey | quote }} + oidc_hmac_secret: {{ .Values.secrets.oidcHmacSecret | quote }} + oidc_private_key: {{ .Values.secrets.oidcPrivateKey | quote }} + {{- if eq .Values.storage.type "postgresql" }} + db_password: {{ .Values.secrets.dbPassword | quote }} + {{- end }} + {{- if .Values.notifier.smtp.enabled }} + smtp_password: {{ .Values.secrets.smtpPassword | quote }} + {{- end }} diff --git a/addons/authelia/role/chart/templates/secret-users.yaml b/addons/authelia/role/chart/templates/secret-users.yaml new file mode 100644 index 0000000..a53bfec --- /dev/null +++ b/addons/authelia/role/chart/templates/secret-users.yaml @@ -0,0 +1,25 @@ +--- +# Authelia users_database.yml — file-based authentication backend. +# Passwords must be Argon2id hashes (see README for generation command). +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "authelia.name" . }}-users + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +type: Opaque +stringData: + users_database.yml: | + users: + {{- range $username, $user := .Values.users }} + {{ $username }}: + disabled: {{ $user.disabled | default false }} + displayname: {{ $user.displayname | quote }} + password: {{ $user.password | quote }} + email: {{ $user.email | quote }} + groups: + {{- range $user.groups }} + - {{ . | quote }} + {{- end }} + {{- end }} diff --git a/addons/authelia/role/chart/templates/service.yaml b/addons/authelia/role/chart/templates/service.yaml new file mode 100644 index 0000000..5727753 --- /dev/null +++ b/addons/authelia/role/chart/templates/service.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "authelia.name" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "authelia.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "authelia.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: 9091 + targetPort: 9091 + protocol: TCP diff --git a/addons/authelia/role/chart/values.yaml b/addons/authelia/role/chart/values.yaml new file mode 100644 index 0000000..348660f --- /dev/null +++ b/addons/authelia/role/chart/values.yaml @@ -0,0 +1,229 @@ +# Authelia — SSO + Forward Auth +# Configure via: group_vars/all/addons.yml → authelia_* variables +# Secrets in: group_vars/all/vault.yml → authelia_* secrets + +image: + repository: authelia/authelia + tag: "4.38.17" + pullPolicy: IfNotPresent + +# ── Domain ──────────────────────────────────────────────────────────────────── +# Base domain and public hostname for the Authelia portal +domain: "home.local" +authHost: "auth.home.local" + +# ── Theme / UX ──────────────────────────────────────────────────────────────── +theme: dark # light | dark | grey | auto + +# ── Secrets (all provided via Ansible vault, never hardcoded here) ──────────── +# These are placed in a dedicated K8s Secret and mounted as files. +# Authelia reads them via AUTHELIA_*_FILE environment variables. +secrets: + jwtSecret: "" # min 64 chars — `openssl rand -base64 64` + sessionSecret: "" # min 64 chars — `openssl rand -base64 64` + storageEncryptionKey: "" # min 20 chars — `openssl rand -base64 32` + oidcHmacSecret: "" # min 32 chars — `openssl rand -base64 48` + oidcPrivateKey: "" # RSA-4096 PEM — auto-generated during deploy + dbPassword: "" # only if storage.type=postgresql + smtpPassword: "" # only if notifier.smtp.enabled + +# ── Users (file-based auth backend) ────────────────────────────────────────── +# Passwords must be Argon2id hashes. +# Generate: docker run authelia/authelia:latest authelia hash-password 'yourpassword' +# Or: authelia hash-password 'yourpassword' (if installed locally) +users: + admin: + disabled: false + displayname: "Administrator" + password: "" # set in vault: authelia_user_admin_password_hash (argon2id) + email: "admin@home.local" + groups: + - admins + - users + +# ── TOTP ────────────────────────────────────────────────────────────────────── +totp: + issuer: "home.local" + period: 30 + skew: 1 + +# ── 2FA policy ──────────────────────────────────────────────────────────────── +# false = one_factor (password only) for protected/admin domains +# true = two_factor (password + TOTP/WebAuthn) for protected/admin domains +twoFactor: + enabled: false + +# ── Session ─────────────────────────────────────────────────────────────────── +session: + name: authelia_session + domain: "home.local" + sameSite: lax + expiration: 1h + inactivity: 5m + rememberMeDuration: 1M + +# ── Storage ─────────────────────────────────────────────────────────────────── +storage: + type: sqlite # sqlite | postgresql + size: 1Gi + storageClassName: "" + sqlite: + path: /data/db.sqlite3 + postgresql: + host: "postgresql.postgresql.svc.cluster.local" + port: 5432 + database: authelia + username: authelia + schema: public + +# ── Redis (built-in, optional) ──────────────────────────────────────────────── +# When enabled, deploys a Redis sidecar for persistent session storage. +# Recommended for production; not required for homelab. +redis: + enabled: false + image: "redis:7-alpine" + resources: + requests: + cpu: 20m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + +# ── Notifier ────────────────────────────────────────────────────────────────── +notifier: + # SMTP — for password reset and 2FA enrollment emails + smtp: + enabled: false + host: "" + port: 587 + username: "" + sender: "authelia@home.local" + tls: + skipVerify: false + # Filesystem — fallback: writes to /data/notification.txt (no email) + filesystem: + enabled: true + +# ── Access Control ──────────────────────────────────────────────────────────── +# Rules evaluated top-to-bottom. First match wins. +accessControl: + defaultPolicy: deny + + # Public services — no authentication required + bypassDomains: [] + # - plex.home.local + + # OIDC-enabled services — bypass forward-auth (OIDC handles its own auth) + oidcDomains: + - gitea.home.local + - grafana.home.local + - minio.home.local + + # Services requiring admin group membership + adminDomains: + - argocd.home.local + - vault.home.local + - harbor.home.local + - kubernetes-dashboard.home.local + + # Services requiring login (one_factor or two_factor per twoFactor.enabled) + protectedDomains: + - sonarr.home.local + - radarr.home.local + - lidarr.home.local + - bazarr.home.local + - prowlarr.home.local + - pgadmin.home.local + - phpmyadmin.home.local + +# ── OIDC Provider ───────────────────────────────────────────────────────────── +oidc: + enabled: true + accessTokenLifespan: 1h + authorizeCodeLifespan: 1m + idTokenLifespan: 1h + refreshTokenLifespan: 90m + + clients: + gitea: + enabled: true + id: gitea + secret: "" # set in vault: authelia_oidc_secret_gitea + description: "Gitea" + redirectUris: + - https://gitea.home.local/user/oauth2/Authelia/callback + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + + grafana: + enabled: true + id: grafana + secret: "" # set in vault: authelia_oidc_secret_grafana + description: "Grafana" + redirectUris: + - https://grafana.home.local/login/generic_oauth + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + + argocd: + enabled: false + id: argocd + secret: "" # set in vault: authelia_oidc_secret_argocd + description: "ArgoCD" + redirectUris: + - https://argocd.home.local/auth/callback + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + + minio: + enabled: false + id: minio + secret: "" # set in vault: authelia_oidc_secret_minio + description: "MinIO" + redirectUris: + - https://minio.home.local/oauth_callback + scopes: [openid, profile, email] + grantTypes: [authorization_code] + + vault: + enabled: false + id: vault + secret: "" # set in vault: authelia_oidc_secret_vault + description: "Vault" + redirectUris: + - https://vault.home.local/ui/vault/auth/oidc/oidc/callback + - https://vault.home.local/oidc/callback + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + + nextcloud: + enabled: false + id: nextcloud + secret: "" # set in vault: authelia_oidc_secret_nextcloud + description: "Nextcloud" + redirectUris: + - https://nextcloud.home.local/apps/user_oidc/code + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + +# ── Ingress ─────────────────────────────────────────────────────────────────── +ingress: + enabled: true + ingressClass: nginx + tls: + enabled: false + secretName: "" + certManager: + enabled: false + issuer: "" + issuerKind: ClusterIssuer + +# ── Resources ───────────────────────────────────────────────────────────────── +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi diff --git a/addons/authelia/role/defaults/main.yml b/addons/authelia/role/defaults/main.yml new file mode 100644 index 0000000..71d081a --- /dev/null +++ b/addons/authelia/role/defaults/main.yml @@ -0,0 +1,123 @@ +--- +# ── Namespace / release ─────────────────────────────────────────────────────── +authelia_namespace: authelia +authelia_release_name: authelia + +# ── Domain ──────────────────────────────────────────────────────────────────── +authelia_domain: "home.local" +authelia_host: "auth.home.local" + +# ── Theme ───────────────────────────────────────────────────────────────────── +authelia_theme: dark # light | dark | grey | auto + +# ── 2FA enforcement ─────────────────────────────────────────────────────────── +authelia_two_factor_enabled: false + +# ── Session ─────────────────────────────────────────────────────────────────── +authelia_session_expiration: "1h" +authelia_session_inactivity: "5m" +authelia_session_remember_me: "1M" + +# ── Storage ─────────────────────────────────────────────────────────────────── +authelia_storage_type: sqlite # sqlite | postgresql +authelia_storage_size: 1Gi +authelia_storage_class: "" + +# PostgreSQL (only if authelia_storage_type=postgresql) +authelia_db_host: "postgresql.postgresql.svc.cluster.local" +authelia_db_port: 5432 +authelia_db_name: authelia +authelia_db_user: authelia + +# ── Redis session storage (optional) ───────────────────────────────────────── +authelia_redis_enabled: false + +# ── SMTP notifier (optional) ────────────────────────────────────────────────── +authelia_smtp_enabled: false +authelia_smtp_host: "" +authelia_smtp_port: 587 +authelia_smtp_username: "" +authelia_smtp_sender: "authelia@home.local" +authelia_smtp_tls_skip_verify: false + +# ── Ingress ─────────────────────────────────────────────────────────────────── +authelia_ingress_enabled: true +authelia_ingress_class: nginx +authelia_ingress_tls_enabled: false +authelia_ingress_tls_secret: "" +authelia_ingress_cert_manager_enabled: false +authelia_ingress_cert_manager_issuer: "" + +# ── OIDC provider ───────────────────────────────────────────────────────────── +authelia_oidc_enabled: true + +# Per-client toggles — secrets come from vault.yml +authelia_oidc_gitea_enabled: true +authelia_oidc_gitea_redirect: "https://gitea.home.local/user/oauth2/Authelia/callback" + +authelia_oidc_grafana_enabled: true +authelia_oidc_grafana_redirect: "https://grafana.home.local/login/generic_oauth" + +authelia_oidc_argocd_enabled: false +authelia_oidc_argocd_redirect: "https://argocd.home.local/auth/callback" + +authelia_oidc_minio_enabled: false +authelia_oidc_minio_redirect: "https://minio.home.local/oauth_callback" + +authelia_oidc_vault_enabled: false +authelia_oidc_vault_redirect_1: "https://vault.home.local/ui/vault/auth/oidc/oidc/callback" +authelia_oidc_vault_redirect_2: "https://vault.home.local/oidc/callback" + +authelia_oidc_nextcloud_enabled: false +authelia_oidc_nextcloud_redirect: "https://nextcloud.home.local/apps/user_oidc/code" + +# ── Access control ──────────────────────────────────────────────────────────── +authelia_bypass_domains: [] +# - plex.home.local + +authelia_oidc_domains: + - gitea.home.local + - grafana.home.local + - minio.home.local + +authelia_admin_domains: + - argocd.home.local + - vault.home.local + - harbor.home.local + - kubernetes-dashboard.home.local + +authelia_protected_domains: + - sonarr.home.local + - radarr.home.local + - lidarr.home.local + - bazarr.home.local + - prowlarr.home.local + - pgadmin.home.local + - phpmyadmin.home.local + +# ── Users ───────────────────────────────────────────────────────────────────── +# Passwords are Argon2id hashes — set in vault.yml: authelia_user_*_password_hash +# Generate: docker run authelia/authelia:latest authelia hash-password 'yourpassword' +authelia_users: + admin: + displayname: "Administrator" + email: "admin@home.local" + groups: + - admins + - users + +# ── Secrets — ALL must be set in vault.yml ──────────────────────────────────── +# authelia_jwt_secret: "" # openssl rand -base64 64 +# authelia_session_secret: "" # openssl rand -base64 64 +# authelia_storage_encryption_key: "" # openssl rand -base64 32 +# authelia_oidc_hmac_secret: "" # openssl rand -base64 48 +# authelia_oidc_private_key: "" # auto-generated during deploy if empty +# authelia_oidc_secret_gitea: "" # openssl rand -hex 32 +# authelia_oidc_secret_grafana: "" # openssl rand -hex 32 +# authelia_oidc_secret_argocd: "" +# authelia_oidc_secret_minio: "" +# authelia_oidc_secret_vault: "" +# authelia_oidc_secret_nextcloud: "" +# authelia_user_admin_password_hash: "" # argon2id hash +# authelia_db_password: "" # only if storage_type=postgresql +# authelia_smtp_password: "" # only if smtp_enabled=true diff --git a/addons/authelia/role/tasks/main.yml b/addons/authelia/role/tasks/main.yml new file mode 100644 index 0000000..09a5034 --- /dev/null +++ b/addons/authelia/role/tasks/main.yml @@ -0,0 +1,206 @@ +--- +# ── Validate required secrets ───────────────────────────────────────────────── + +- name: Validate Authelia required secrets are set + ansible.builtin.assert: + that: + - authelia_jwt_secret is defined and authelia_jwt_secret | length >= 64 + - authelia_session_secret is defined and authelia_session_secret | length >= 64 + - authelia_storage_encryption_key is defined and authelia_storage_encryption_key | length >= 20 + - authelia_oidc_hmac_secret is defined and authelia_oidc_hmac_secret | length >= 32 + fail_msg: > + Required secrets are missing or too short in vault.yml. + authelia_jwt_secret (min 64 chars) : openssl rand -base64 64 + authelia_session_secret (min 64 chars) : openssl rand -base64 64 + authelia_storage_encryption_key (min 20 chars): openssl rand -base64 32 + authelia_oidc_hmac_secret (min 32 chars) : openssl rand -base64 48 + +- name: Validate admin user password hash is set + ansible.builtin.assert: + that: + - authelia_user_admin_password_hash is defined + - authelia_user_admin_password_hash | length > 0 + fail_msg: > + authelia_user_admin_password_hash is not set in vault.yml. + Generate with: docker run authelia/authelia:latest authelia hash-password 'yourpassword' + Then paste the $argon2id$... hash into vault.yml + +- name: Validate OIDC client secrets when OIDC is enabled + ansible.builtin.assert: + that: + - authelia_oidc_secret_gitea is defined and authelia_oidc_secret_gitea | length >= 16 + - authelia_oidc_secret_grafana is defined and authelia_oidc_secret_grafana | length >= 16 + fail_msg: > + authelia_oidc_secret_gitea and authelia_oidc_secret_grafana must be set in vault.yml. + Generate with: openssl rand -hex 32 + when: authelia_oidc_enabled | bool + +# ── Generate OIDC RSA private key if not provided ──────────────────────────── + +- name: Check if OIDC private key is already set + ansible.builtin.set_fact: + _authelia_oidc_key_provided: >- + {{ authelia_oidc_private_key is defined and + authelia_oidc_private_key | length > 0 }} + +- name: Generate RSA-4096 OIDC private key (if not in vault) + ansible.builtin.command: openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 + register: _generated_oidc_key + changed_when: true + no_log: true + when: + - authelia_oidc_enabled | bool + - not _authelia_oidc_key_provided | bool + +- name: Use generated OIDC private key + ansible.builtin.set_fact: + _authelia_oidc_private_key_final: "{{ _generated_oidc_key.stdout }}" + no_log: true + when: + - authelia_oidc_enabled | bool + - not _authelia_oidc_key_provided | bool + +- name: Use vault-provided OIDC private key + ansible.builtin.set_fact: + _authelia_oidc_private_key_final: "{{ authelia_oidc_private_key }}" + no_log: true + when: _authelia_oidc_key_provided | bool + +- name: Set empty OIDC private key (OIDC disabled) + ansible.builtin.set_fact: + _authelia_oidc_private_key_final: "" + when: not (authelia_oidc_enabled | bool) + +# ── Create namespace ────────────────────────────────────────────────────────── + +- name: Create authelia namespace + ansible.builtin.command: > + k3s kubectl create namespace {{ authelia_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + changed_when: false + +# ── Copy Helm chart ─────────────────────────────────────────────────────────── + +- name: Ensure chart temp directory is clean + ansible.builtin.file: + path: /tmp/authelia-chart + state: absent + become: true + +- name: Create chart temp directory + ansible.builtin.file: + path: /tmp/authelia-chart + state: directory + mode: "0755" + become: true + +- name: Copy Helm chart to master + ansible.builtin.copy: + src: "{{ role_path }}/chart/" + dest: /tmp/authelia-chart/ + mode: preserve + become: true + +# ── Template Helm values ────────────────────────────────────────────────────── + +- name: Template Helm values + ansible.builtin.template: + src: values.yaml.j2 + dest: /tmp/authelia-values.yaml + mode: "0600" + become: true + no_log: true + +# ── Lint chart ──────────────────────────────────────────────────────────────── + +- name: Lint Helm chart + ansible.builtin.command: > + helm lint /tmp/authelia-chart + --values /tmp/authelia-values.yaml + become: true + changed_when: false + register: _helm_lint + failed_when: _helm_lint.rc != 0 + +# ── Deploy chart ────────────────────────────────────────────────────────────── + +- name: Deploy Authelia via Helm + ansible.builtin.command: > + helm upgrade --install {{ authelia_release_name }} + /tmp/authelia-chart + --namespace {{ authelia_namespace }} + --values /tmp/authelia-values.yaml + --atomic + --wait + --timeout 180s + become: true + register: _helm_result + changed_when: true + +# ── Cleanup ─────────────────────────────────────────────────────────────────── + +- name: Remove temp values file (contains secrets) + ansible.builtin.file: + path: /tmp/authelia-values.yaml + state: absent + become: true + +# ── Wait for readiness ──────────────────────────────────────────────────────── + +- name: Wait for Authelia pod to be ready + ansible.builtin.command: > + k3s kubectl -n {{ authelia_namespace }} rollout status + deployment/authelia --timeout=120s + become: true + changed_when: false + +# ── Get status ──────────────────────────────────────────────────────────────── + +- name: Get pod status + ansible.builtin.command: > + k3s kubectl -n {{ authelia_namespace }} get pods,svc,ingress -o wide + become: true + changed_when: false + register: _status + +- name: Get forward-auth annotations reference + ansible.builtin.command: > + k3s kubectl -n {{ authelia_namespace }} get cm authelia-forward-auth + -o jsonpath='{.data.annotations\.yaml}' + become: true + changed_when: false + register: _annotations + +# ── Summary ─────────────────────────────────────────────────────────────────── + +- name: "=== Authelia SSO Ready ===" + ansible.builtin.debug: + msg: + - "╔══════════════════════════════════════════════════════════════╗" + - "║ Authelia SSO — Deployed ║" + - "╚══════════════════════════════════════════════════════════════╝" + - "" + - " Portal : http{{ 's' if authelia_ingress_tls_enabled else '' }}://{{ authelia_host }}/" + - " Namespace : {{ authelia_namespace }}" + - " OIDC : {{ 'enabled' if authelia_oidc_enabled else 'disabled' }}" + - " Storage : {{ authelia_storage_type }}" + - " Redis : {{ 'enabled' if authelia_redis_enabled else 'disabled' }}" + - "" + - " ── Protect a new service (add to its Ingress) ──" + - "{{ _annotations.stdout_lines | to_yaml }}" + - "" + - " ── OIDC Issuer URL ──" + - " http{{ 's' if authelia_ingress_tls_enabled else '' }}://{{ authelia_host }}" + - "" + - " ── Pods / Services ──" + - "{{ _status.stdout_lines | to_yaml }}" + - "" + - " Login: open http{{ 's' if authelia_ingress_tls_enabled else '' }}://{{ authelia_host }}/" + - " user: admin | pass: " + - "" + - "{% if not _authelia_oidc_key_provided %}" + - " ⚠ OIDC private key was AUTO-GENERATED. Save it to vault.yml for reproducibility:" + - " kubectl -n {{ authelia_namespace }} get secret authelia-secrets \\" + - " -o jsonpath='{.data.oidc_private_key}' | base64 -d" + - "{% endif %}" diff --git a/addons/authelia/role/templates/values.yaml.j2 b/addons/authelia/role/templates/values.yaml.j2 new file mode 100644 index 0000000..7d2f873 --- /dev/null +++ b/addons/authelia/role/templates/values.yaml.j2 @@ -0,0 +1,154 @@ +# Generated by Ansible — do not edit manually. +# Configure via: group_vars/all/addons.yml → authelia_* variables +# Secrets from: group_vars/all/vault.yml → authelia_* secrets + +domain: {{ authelia_domain | quote }} +authHost: {{ authelia_host | quote }} +theme: {{ authelia_theme | quote }} + +secrets: + jwtSecret: {{ authelia_jwt_secret | quote }} + sessionSecret: {{ authelia_session_secret | quote }} + storageEncryptionKey: {{ authelia_storage_encryption_key | quote }} + oidcHmacSecret: {{ authelia_oidc_hmac_secret | quote }} + oidcPrivateKey: {{ _authelia_oidc_private_key_final | quote }} + dbPassword: {{ authelia_db_password | default('') | quote }} + smtpPassword: {{ authelia_smtp_password | default('') | quote }} + +# Users — passwords are Argon2id hashes from vault.yml +users: +{% for username, user in authelia_users.items() %} + {{ username }}: + disabled: false + displayname: {{ user.displayname | quote }} + password: {{ vars['authelia_user_' + username + '_password_hash'] | default('') | quote }} + email: {{ user.email | quote }} + groups: +{{ user.groups | to_yaml | indent(6, True) }} +{% endfor %} + +totp: + issuer: {{ authelia_domain | quote }} + period: 30 + skew: 1 + +twoFactor: + enabled: {{ authelia_two_factor_enabled | string | lower }} + +session: + name: authelia_session + domain: {{ authelia_domain | quote }} + sameSite: lax + expiration: {{ authelia_session_expiration | quote }} + inactivity: {{ authelia_session_inactivity | quote }} + rememberMeDuration: {{ authelia_session_remember_me | quote }} + +storage: + type: {{ authelia_storage_type | quote }} + size: {{ authelia_storage_size | quote }} + storageClassName: {{ authelia_storage_class | quote }} + sqlite: + path: /data/db.sqlite3 + postgresql: + host: {{ authelia_db_host | quote }} + port: {{ authelia_db_port }} + database: {{ authelia_db_name | quote }} + username: {{ authelia_db_user | quote }} + schema: public + +redis: + enabled: {{ authelia_redis_enabled | string | lower }} + +notifier: + smtp: + enabled: {{ authelia_smtp_enabled | string | lower }} + host: {{ authelia_smtp_host | quote }} + port: {{ authelia_smtp_port }} + username: {{ authelia_smtp_username | quote }} + sender: {{ authelia_smtp_sender | quote }} + tls: + skipVerify: {{ authelia_smtp_tls_skip_verify | string | lower }} + +accessControl: + defaultPolicy: deny + bypassDomains: +{{ authelia_bypass_domains | to_yaml | indent(4, True) }} + oidcDomains: +{{ authelia_oidc_domains | to_yaml | indent(4, True) }} + adminDomains: +{{ authelia_admin_domains | to_yaml | indent(4, True) }} + protectedDomains: +{{ authelia_protected_domains | to_yaml | indent(4, True) }} + +oidc: + enabled: {{ authelia_oidc_enabled | string | lower }} + accessTokenLifespan: 1h + authorizeCodeLifespan: 1m + idTokenLifespan: 1h + refreshTokenLifespan: 90m + clients: + gitea: + enabled: {{ authelia_oidc_gitea_enabled | string | lower }} + id: gitea + secret: {{ authelia_oidc_secret_gitea | default('') | quote }} + description: "Gitea" + redirectUris: + - {{ authelia_oidc_gitea_redirect | quote }} + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + grafana: + enabled: {{ authelia_oidc_grafana_enabled | string | lower }} + id: grafana + secret: {{ authelia_oidc_secret_grafana | default('') | quote }} + description: "Grafana" + redirectUris: + - {{ authelia_oidc_grafana_redirect | quote }} + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + argocd: + enabled: {{ authelia_oidc_argocd_enabled | string | lower }} + id: argocd + secret: {{ authelia_oidc_secret_argocd | default('') | quote }} + description: "ArgoCD" + redirectUris: + - {{ authelia_oidc_argocd_redirect | quote }} + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + minio: + enabled: {{ authelia_oidc_minio_enabled | string | lower }} + id: minio + secret: {{ authelia_oidc_secret_minio | default('') | quote }} + description: "MinIO" + redirectUris: + - {{ authelia_oidc_minio_redirect | quote }} + scopes: [openid, profile, email] + grantTypes: [authorization_code] + vault: + enabled: {{ authelia_oidc_vault_enabled | string | lower }} + id: vault + secret: {{ authelia_oidc_secret_vault | default('') | quote }} + description: "Vault" + redirectUris: + - {{ authelia_oidc_vault_redirect_1 | quote }} + - {{ authelia_oidc_vault_redirect_2 | quote }} + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + nextcloud: + enabled: {{ authelia_oidc_nextcloud_enabled | string | lower }} + id: nextcloud + secret: {{ authelia_oidc_secret_nextcloud | default('') | quote }} + description: "Nextcloud" + redirectUris: + - {{ authelia_oidc_nextcloud_redirect | quote }} + scopes: [openid, profile, email, groups] + grantTypes: [refresh_token, authorization_code] + +ingress: + enabled: {{ authelia_ingress_enabled | string | lower }} + ingressClass: {{ authelia_ingress_class | quote }} + tls: + enabled: {{ authelia_ingress_tls_enabled | string | lower }} + secretName: {{ authelia_ingress_tls_secret | quote }} + certManager: + enabled: {{ authelia_ingress_cert_manager_enabled | string | lower }} + issuer: {{ authelia_ingress_cert_manager_issuer | quote }} diff --git a/group_vars/all/addons.yml b/group_vars/all/addons.yml index 83cded0..57a9c32 100644 --- a/group_vars/all/addons.yml +++ b/group_vars/all/addons.yml @@ -45,6 +45,7 @@ addon_ingress_proxypass: false # External Services Ingress Proxy — addon_ingress_add_domains: false # Ingress-only — добавить домены к существующим сервисам кластера addon_yandex_dns_controller: false # Yandex 360 DNS controller — управление DNS через ConfigMap (safe mode) addon_technitium_dns: false # Technitium DNS HA — Primary+Secondary с kube-vip LB, зональный sync +addon_authelia: false # Authelia SSO — Forward-auth + OIDC provider для всех сервисов # ─── NFS Server ─────────────────────────────────────────────────────────────── nfs_exports: @@ -381,6 +382,32 @@ minio_api_ingress_host: "s3.example.com" # technitium_dns_externaldns_policy: "upsert-only" # sync | upsert-only # technitium_dns_externaldns_txt_owner_id: "k3s-home" +# ─── Authelia SSO ──────────────────────────────────────────────────────────── +# Централизованная аутентификация: forward-auth для ingress-nginx + OIDC provider. +# Все секреты — в vault.yml (authelia_jwt_secret, authelia_session_secret, и др.) +# authelia_host: "auth.home.local" # URL портала авторизации +# authelia_domain: "home.local" # базовый домен (session cookie domain) +# authelia_two_factor_enabled: false # включить 2FA для защищённых сервисов +# authelia_storage_type: "sqlite" # sqlite | postgresql +# authelia_redis_enabled: false # Redis для хранения сессий +# authelia_smtp_enabled: false # SMTP для сброса пароля и 2FA email +# Домены с защитой (forward-auth): +# authelia_protected_domains: [sonarr.home.local, radarr.home.local, ...] +# authelia_admin_domains: [argocd.home.local, vault.home.local, ...] +# authelia_bypass_domains: [plex.home.local] +# OIDC клиенты (включить нужные): +# authelia_oidc_gitea_enabled: true # + authelia_oidc_secret_gitea в vault +# authelia_oidc_grafana_enabled: true # + authelia_oidc_secret_grafana в vault +# authelia_oidc_argocd_enabled: false +# authelia_oidc_minio_enabled: false +# authelia_oidc_vault_enabled: false +# Пользователи (хэши паролей — в vault.yml): +# authelia_users: +# admin: +# displayname: "Administrator" +# email: "admin@home.local" +# groups: [admins, users] + # ─── etcd backup ────────────────────────────────────────────────────────────── etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots" etcd_backup_retention: 5 # сколько снимков хранить diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example index 1873d81..10bc08e 100644 --- a/group_vars/all/vault.yml.example +++ b/group_vars/all/vault.yml.example @@ -135,3 +135,26 @@ yandex_dns: # ── Technitium DNS HA ───────────────────────────────────────────────────────── technitium_dns_admin_password: "ЗАМЕНИ_НА_ПАРОЛЬ" # минимум 8 символов + +# ── Authelia SSO ────────────────────────────────────────────────────────────── +# Generate secrets: +# openssl rand -base64 64 → jwt_secret, session_secret +# openssl rand -base64 32 → storage_encryption_key +# openssl rand -base64 48 → oidc_hmac_secret +# openssl rand -hex 32 → each OIDC client secret +# Generate password hash: +# docker run authelia/authelia:latest authelia hash-password 'yourpassword' +authelia_jwt_secret: "ЗАМЕНИ_openssl_rand_-base64_64" +authelia_session_secret: "ЗАМЕНИ_openssl_rand_-base64_64" +authelia_storage_encryption_key: "ЗАМЕНИ_openssl_rand_-base64_32" +authelia_oidc_hmac_secret: "ЗАМЕНИ_openssl_rand_-base64_48" +authelia_oidc_private_key: "" # оставь пустым — будет сгенерирован автоматически +# OIDC client secrets (генерируй для каждого включённого клиента): +authelia_oidc_secret_gitea: "ЗАМЕНИ_openssl_rand_-hex_32" +authelia_oidc_secret_grafana: "ЗАМЕНИ_openssl_rand_-hex_32" +authelia_oidc_secret_argocd: "" +authelia_oidc_secret_minio: "" +authelia_oidc_secret_vault: "" +authelia_oidc_secret_nextcloud: "" +# User password hashes (argon2id): +authelia_user_admin_password_hash: "ЗАМЕНИ_НА_ARGON2ID_ХЭШ" diff --git a/playbooks/addons.yml b/playbooks/addons.yml index 0a0fba5..89725d5 100644 --- a/playbooks/addons.yml +++ b/playbooks/addons.yml @@ -327,3 +327,11 @@ when: addon_technitium_dns | default(false) | bool roles: - role: "{{ playbook_dir }}/../addons/technitium-dns/role" + +- name: Install Authelia SSO + hosts: k3s_master[0] + gather_facts: false + become: true + when: addon_authelia | default(false) | bool + roles: + - role: "{{ playbook_dir }}/../addons/authelia/role"