diff --git a/Caddyfile b/Caddyfile index c72a48a..e3380c5 100644 --- a/Caddyfile +++ b/Caddyfile @@ -210,13 +210,7 @@ storage.j-costa.com, https://storage.j-costa.com { # MINIO CONSOLE — internal only # ============================================================= minio.ginnoir.com { - route { - import authentik_outpost - handle { - import authentik_forward_auth - reverse_proxy resume-minio:9001 - } - } + reverse_proxy resume-minio:9001 } # ============================================================= @@ -238,19 +232,15 @@ dev.ginnoir.com { # MANAGEMENT — internal only # ============================================================= portainer.ginnoir.com { - route { - import authentik_outpost - @webhook { - method POST - path /api/stacks/webhooks/* - } - handle @webhook { - reverse_proxy portainer:9000 - } - handle { - import authentik_forward_auth - reverse_proxy portainer:9000 - } + @webhook { + method POST + path /api/stacks/webhooks/* + } + handle @webhook { + reverse_proxy portainer:9000 + } + handle { + reverse_proxy portainer:9000 } } @@ -265,13 +255,7 @@ uptime.ginnoir.com { } homarr.ginnoir.com { - route { - import authentik_outpost - handle { - import authentik_forward_auth - reverse_proxy homarr:7575 - } - } + reverse_proxy homarr:7575 } backup.ginnoir.com { @@ -293,20 +277,16 @@ router.ginnoir.com { # DEV STACK — internal only # ============================================================= gitea.ginnoir.com { - route { - import authentik_outpost - @api path /api/* - @git path_regexp (?i)\.git(/|$) - handle @api { - reverse_proxy gitea:3000 - } - handle @git { - reverse_proxy gitea:3000 - } - handle { - import authentik_forward_auth - reverse_proxy gitea:3000 - } + @api path /api/* + @git path_regexp (?i)\.git(/|$) + handle @api { + reverse_proxy gitea:3000 + } + handle @git { + reverse_proxy gitea:3000 + } + handle { + reverse_proxy gitea:3000 } } @@ -360,28 +340,18 @@ vault.ginnoir.com { } docs.ginnoir.com { - route { - import authentik_outpost - handle { - import authentik_forward_auth - reverse_proxy bookstack:80 - } - } + reverse_proxy bookstack:80 } plane.ginnoir.com { - route { - import authentik_outpost - handle /api/* { - reverse_proxy plane_api:8000 - } - handle /auth/* { - reverse_proxy plane_api:8000 - } - handle { - import authentik_forward_auth - reverse_proxy plane_web:3000 - } + handle /api/* { + reverse_proxy plane_api:8000 + } + handle /auth/* { + reverse_proxy plane_api:8000 + } + handle { + reverse_proxy plane_web:3000 } } diff --git a/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md b/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md index 1bede1a..e3924cb 100644 --- a/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md +++ b/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md @@ -53,7 +53,7 @@ | | | |---|---| | **Decision** | Do it — batch 1 admin deployed; batches 2–3 pending | -| **Notes** | Out: obsidian, foundry×2, overseerr. Batch 1: 12 admin sites + Caddy forward_auth + Authentik proxy providers. Push repo to persist authentik compose/blueprint changes. | +| **Notes** | Out: obsidian, foundry×2, overseerr. Batch 1: homarr native OIDC done; rest split native OAuth vs forward_auth+disable local auth (2026-06-11). Portainer needs one-time OAuth in UI; Plane needs god-mode OIDC. Push repo to persist. | | **Date** | 2026-06-10 (batch 1 deployed) | --- @@ -69,8 +69,9 @@ Caddyfile + Authentik UI 1. Authentik embedded outpost + proxy provider(s) on `edge` 2. Caddy `(authentik_auth)` snippet — `forward_auth authentik-server:9000` with trusted headers -3. Roll out site-by-site; per-site `handle` bypasses for webhooks/API (`/api/*` where key-auth, Portainer `/api/stacks/webhooks/*`, Gitea hooks, ntfy publish, etc.) -4. Disable or bypass redundant in-app login where the app supports it (optional second pass) +3. Roll out site-by-site; per-site `handle` bypasses for webhooks/API +4. **Important:** `forward_auth` only gates the edge — apps still show their own login unless you add **native OIDC/OAuth** (famapp/homarr pattern) or disable local auth (e.g. code-server `--auth none`). Homarr switched to native OIDC 2026-06-11. +5. **Batch 1 rest (2026-06-11):** BookStack/Gitea/MinIO/Portainer/Plane → native OAuth2 providers; code/uptime/kopia/registry-ui/dbx/vault → forward_auth with local auth disabled where possible. **Portainer:** one-time OAuth setup in Settings → Authentication (see `setup-batch1-oidc.py` output). **Plane:** god-mode at `/god-mode/authentication/oidc`. **Rollout batches** @@ -84,7 +85,13 @@ Caddyfile + Authentik UI ### Done when -- [x] Batch 1 admin sites require Authentik before app UI loads +- [x] Batch 1 admin sites require Authentik before app UI loads (or native OAuth equivalent) +- [x] Homarr native OIDC (no double login) +- [x] BookStack, Gitea, MinIO console → native Authentik OAuth/OIDC +- [x] code-server, uptime, kopia → forward_auth only (local auth disabled) +- [x] registry-ui, dbx, vault → forward_auth at edge +- [ ] Portainer OAuth configured in Settings → Authentication (provider created; UI step pending) +- [ ] Plane OIDC configured in god-mode (provider created; UI step pending) - [x] Portainer git webhooks still work (bypass path) - [x] Gitea `/api/*` + `.git` and Vault `/v1/*` bypass forward_auth - [ ] Batch 2 media sites diff --git a/stacks/authentik/scripts/apply-batch1-live.sh b/stacks/authentik/scripts/apply-batch1-live.sh new file mode 100644 index 0000000..dbe1f0e --- /dev/null +++ b/stacks/authentik/scripts/apply-batch1-live.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e + +docker cp /tmp/disable-uptime-auth.py uptime_kuma:/tmp/disable-uptime-auth.py +docker exec uptime_kuma python3 /tmp/disable-uptime-auth.py /app/data/kuma.db + +docker exec -u git gitea gitea admin auth add-oauth \ + --name Authentik \ + --provider openidConnect \ + --key y7rEvQjZ2J6sGuLndPZalGXn4jx0Tus601U9USOS \ + --secret FCbRN0YUh5K6rGhirX5HlrSumYgmKPGi6xBPQY5YvpJIPGi9st4g8FZN2kyJ7IzDXbj0FGmE3Q2qysNcZYePc7mofq6k98fz9Ue97Tjj4y3ZPNXAeAqlJrclv9DzR6eo \ + --auto-discover-url https://auth.ginnoir.com/application/o/gitea/.well-known/openid-configuration \ + --scopes "openid profile email" || echo "gitea oauth may already exist" + +echo done diff --git a/stacks/authentik/scripts/apply-forward-auth-admin.py b/stacks/authentik/scripts/apply-forward-auth-admin.py index c886f82..2025792 100644 --- a/stacks/authentik/scripts/apply-forward-auth-admin.py +++ b/stacks/authentik/scripts/apply-forward-auth-admin.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 -"""Apply TB-006 batch 1 proxy providers via authentik ORM (run: ak shell < script).""" +"""Apply TB-006 batch 1 forward_auth proxy providers (forward_auth-only sites). + +Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, plane, homarr) use +setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read()) +""" from authentik.core.models import Application from authentik.flows.models import Flow from authentik.outposts.models import Outpost, OutpostType @@ -8,33 +12,38 @@ from authentik.providers.proxy.models import ProxyMode, ProxyProvider AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent") INVALID = Flow.objects.get(slug="default-provider-invalidation-flow") +UPTIME_UNAUTH = """^/status/.* +^/assets/.* +^/api/push/.* +^/api/badge/.* +^/api/status-page/heartbeat/.* +^/icon.svg +^/upload/.*""" + SITES = [ - ("homelab-portainer", "portainer", "Portainer", "https://portainer.ginnoir.com"), - ("homelab-code", "code", "code-server", "https://code.ginnoir.com"), - ("homelab-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com"), - ("homelab-vault", "vault", "Vault", "https://vault.ginnoir.com"), - ("homelab-minio", "minio", "MinIO Console", "https://minio.ginnoir.com"), - ("homelab-homarr", "homarr", "Homarr", "https://homarr.ginnoir.com"), - ("homelab-uptime", "uptime", "Uptime Kuma", "https://uptime.ginnoir.com"), - ("homelab-backup", "backup", "Kopia", "https://backup.ginnoir.com"), - ("homelab-gitea", "gitea", "Gitea", "https://gitea.ginnoir.com"), - ("homelab-dbx", "dbx", "DBX", "https://dbx.ginnoir.com"), - ("homelab-bookstack", "bookstack", "BookStack", "https://docs.ginnoir.com"), - ("homelab-plane", "plane", "Plane", "https://plane.ginnoir.com"), + ("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None), + ("homelab-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None), + ("homelab-vault", "vault", "Vault", "https://vault.ginnoir.com", None, None), + ("homelab-uptime", "uptime", "Uptime Kuma", "https://uptime.ginnoir.com", "http://uptime-kuma:3001", UPTIME_UNAUTH), + ("homelab-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None), + ("homelab-dbx", "dbx", "DBX", "https://dbx.ginnoir.com", None, None), ] providers = [] -for pname, slug, aname, host in SITES: - provider, _ = ProxyProvider.objects.update_or_create( - name=pname, - defaults={ - "mode": ProxyMode.FORWARD_SINGLE, - "external_host": host, - "authorization_flow": AUTHZ, - "invalidation_flow": INVALID, - "intercept_header_auth": True, - }, - ) +for pname, slug, aname, host, internal, unauth in SITES: + defaults = { + "mode": ProxyMode.FORWARD_SINGLE, + "external_host": host, + "authorization_flow": AUTHZ, + "invalidation_flow": INVALID, + "intercept_header_auth": True, + } + if internal: + defaults["internal_host"] = internal + if unauth: + defaults["skip_path_regex"] = unauth + + provider, _ = ProxyProvider.objects.update_or_create(name=pname, defaults=defaults) Application.objects.update_or_create( slug=slug, defaults={ diff --git a/stacks/authentik/scripts/configure-portainer-oauth.py b/stacks/authentik/scripts/configure-portainer-oauth.py new file mode 100644 index 0000000..b95d85a --- /dev/null +++ b/stacks/authentik/scripts/configure-portainer-oauth.py @@ -0,0 +1,45 @@ +"""Configure Portainer OAuth via API (run on valhalla with admin JWT).""" +import json +import sys +import urllib.error +import urllib.request + +JWT = sys.argv[1] if len(sys.argv) > 1 else "" +CLIENT_ID = sys.argv[2] if len(sys.argv) > 2 else "" +CLIENT_SECRET = sys.argv[3] if len(sys.argv) > 3 else "" +if not JWT or not CLIENT_ID or not CLIENT_SECRET: + print("usage: configure-portainer-oauth.py ") + sys.exit(1) + +BASE = "http://localhost:9100/api" +AUTH = "https://auth.ginnoir.com/application/o" + +payload = { + "AuthenticationMethod": 3, + "OAuthSettings": { + "ClientID": CLIENT_ID, + "ClientSecret": CLIENT_SECRET, + "AuthorizationURI": f"{AUTH}/authorize/", + "AccessTokenURI": f"{AUTH}/token/", + "ResourceURI": f"{AUTH}/userinfo/", + "RedirectURI": "https://portainer.ginnoir.com/", + "LogoutURI": f"{AUTH}/portainer/end-session/", + "UserIdentifier": "email", + "Scopes": "openid email profile", + "OAuthAutoCreateUsers": True, + "DefaultTeamID": 0, + }, +} + +req = urllib.request.Request( + f"{BASE}/settings", + data=json.dumps(payload).encode(), + headers={"Authorization": f"Bearer {JWT}", "Content-Type": "application/json"}, + method="PUT", +) +try: + with urllib.request.urlopen(req) as resp: + print("portainer oauth configured:", resp.status) +except urllib.error.HTTPError as e: + print("error:", e.code, e.read().decode()) + sys.exit(1) diff --git a/stacks/authentik/scripts/deploy-batch1-live.sh b/stacks/authentik/scripts/deploy-batch1-live.sh new file mode 100644 index 0000000..2a99651 --- /dev/null +++ b/stacks/authentik/scripts/deploy-batch1-live.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +docker cp /tmp/disable-uptime-auth.py uptime_kuma:/tmp/disable-uptime-auth.py +docker exec uptime_kuma python3 /tmp/disable-uptime-auth.py /app/data/kuma.db + +docker exec -u git gitea gitea admin auth add-oauth \ + --name Authentik \ + --provider openidConnect \ + --key y7rEvQjZ2J6sGuLndPZalGXn4jx0Tus601U9USOS \ + --secret FCbRN0YUh5K6rGhirX5HlrSumYgmKPGi6xBPQY5YvpJIPGi9st4g8FZN2kyJ7IzDXbj0FGmE3Q2qysNcZYePc7mofq6k98fz9Ue97Tjj4y3ZPNXAeAqlJrclv9DzR6eo \ + --auto-discover-url https://auth.ginnoir.com/application/o/gitea/.well-known/openid-configuration \ + --scopes "openid profile email" 2>&1 || echo "gitea oauth may already exist" + +cp /tmp/init-kopia.sh /config/backup/init-kopia.sh +chmod +x /config/backup/init-kopia.sh +mkdir -p /config/code-server/.config/code-server +cp /tmp/code-server.yaml /config/code-server/code-server.yaml + +docker compose -p dev -f /tmp/batch1-deploy/dev/docker-compose.yml --env-file /tmp/batch1-deploy/dev/stack.env up -d --force-recreate bookstack code-server gitea +docker compose -p resume -f /tmp/batch1-deploy/resume/docker-compose.yml --env-file /tmp/batch1-deploy/resume/stack.env up -d --force-recreate resume-minio +docker compose -p backup -f /data/compose/29/stacks/backup/docker-compose.yml up -d --force-recreate kopia + +docker exec caddy caddy reload --config /etc/caddy/Caddyfile + +echo batch1 live deploy done diff --git a/stacks/authentik/scripts/disable-uptime-auth.py b/stacks/authentik/scripts/disable-uptime-auth.py new file mode 100644 index 0000000..f6fec93 --- /dev/null +++ b/stacks/authentik/scripts/disable-uptime-auth.py @@ -0,0 +1,15 @@ +"""Disable Uptime Kuma local auth (forward_auth is the gate).""" +import sqlite3 +import sys + +DB = sys.argv[1] if len(sys.argv) > 1 else "/config/uptime-kuma/kuma.db" + +conn = sqlite3.connect(DB) +cur = conn.cursor() +cur.execute( + "INSERT INTO setting (key, value) VALUES ('disableAuth', '1') " + "ON CONFLICT(key) DO UPDATE SET value='1'" +) +conn.commit() +conn.close() +print(f"disableAuth=1 in {DB}") diff --git a/stacks/authentik/scripts/setup-batch1-oidc.py b/stacks/authentik/scripts/setup-batch1-oidc.py new file mode 100644 index 0000000..83377b8 --- /dev/null +++ b/stacks/authentik/scripts/setup-batch1-oidc.py @@ -0,0 +1,77 @@ +"""Create OAuth2 providers for batch-1 apps with native SSO (ak shell: exec(open(...).read())).""" +from authentik.core.models import Application +from authentik.flows.models import Flow +from authentik.outposts.models import Outpost +from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode +from authentik.providers.proxy.models import ProxyProvider + +AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent") +INVALID = Flow.objects.get(slug="default-provider-invalidation-flow") +DEFAULT_MAPPINGS = OAuth2Provider.objects.get(name="famapp").property_mappings.all() + +# slug, provider name, redirect URI(s) +OIDC_APPS = [ + ("bookstack", "bookstack", ["https://docs.ginnoir.com/oidc/callback"]), + ("gitea", "gitea", ["https://gitea.ginnoir.com/user/oauth2/Authentik/callback"]), + ("portainer", "portainer", ["https://portainer.ginnoir.com/"]), + ("minio", "minio-console", ["https://minio.ginnoir.com/oauth_callback"]), + ("plane", "plane", ["https://plane.ginnoir.com/auth/oidc/callback/"]), +] + +native_slugs = [] +for slug, pname, redirects in OIDC_APPS: + provider, created = OAuth2Provider.objects.update_or_create( + name=pname, + defaults={ + "authorization_flow": AUTHZ, + "invalidation_flow": INVALID, + "redirect_uris": [ + RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=url) + for url in redirects + ], + "access_code_validity": "minutes=1", + "access_token_validity": "hours=24", + "refresh_token_validity": "days=30", + }, + ) + provider.property_mappings.set(DEFAULT_MAPPINGS) + provider.save() + + app, _ = Application.objects.update_or_create( + slug=slug, + defaults={ + "name": pname.replace("-", " ").title(), + "provider": provider, + "meta_launch_url": redirects[0].split("/oidc")[0].split("/user/oauth2")[0].rstrip("/"), + "policy_engine_mode": "any", + }, + ) + app.provider = provider + app.save() + + native_slugs.append(slug) + print(f"oauth2 {slug} created={created} client_id={provider.client_id}") + print(f" {slug.upper()}_OIDC_CLIENT_ID={provider.client_id}") + print(f" {slug.upper()}_OIDC_CLIENT_SECRET={provider.client_secret}") + +# Drop native-OIDC apps from the embedded proxy outpost (avoid double auth). +PROXY_NAMES = { + "bookstack": "homelab-bookstack", + "gitea": "homelab-gitea", + "portainer": "homelab-portainer", + "minio": "homelab-minio", + "plane": "homelab-plane", +} +outpost = Outpost.objects.get(name="authentik Embedded Outpost") +for slug in native_slugs: + pname = PROXY_NAMES.get(slug) + if not pname: + continue + try: + proxy = ProxyProvider.objects.get(name=pname) + outpost.providers.remove(proxy) + print(f"removed proxy {pname} from embedded outpost") + except ProxyProvider.DoesNotExist: + print(f"proxy {pname} not found (ok)") + +print("done — configure each app with printed client_id/secret") diff --git a/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py b/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py new file mode 100644 index 0000000..fceebfd --- /dev/null +++ b/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py @@ -0,0 +1,59 @@ +"""Update proxy providers for forward_auth-only batch-1 apps (ak shell: exec(open(...).read())).""" +from authentik.core.models import Application +from authentik.flows.models import Flow +from authentik.outposts.models import Outpost, OutpostType +from authentik.providers.proxy.models import ProxyMode, ProxyProvider + +AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent") +INVALID = Flow.objects.get(slug="default-provider-invalidation-flow") + +UPTIME_UNAUTH = """^/status/.* +^/assets/.* +^/api/push/.* +^/api/badge/.* +^/api/status-page/heartbeat/.* +^/icon.svg +^/upload/.*""" + +# Proxy-only sites (native OIDC apps managed by setup-batch1-oidc.py). +SITES = [ + ("homelab-code", "code", "code-server", "https://code.ginnoir.com", None, None), + ("homelab-registry-ui", "registry-ui", "Registry UI", "https://registry-ui.ginnoir.com", None, None), + ("homelab-vault", "vault", "Vault", "https://vault.ginnoir.com", None, None), + ("homelab-uptime", "uptime", "Uptime Kuma", "https://uptime.ginnoir.com", "http://uptime-kuma:3001", UPTIME_UNAUTH), + ("homelab-backup", "backup", "Kopia", "https://backup.ginnoir.com", None, None), + ("homelab-dbx", "dbx", "DBX", "https://dbx.ginnoir.com", None, None), +] + +providers = [] +for pname, slug, aname, host, internal, unauth in SITES: + defaults = { + "mode": ProxyMode.FORWARD_SINGLE, + "external_host": host, + "authorization_flow": AUTHZ, + "invalidation_flow": INVALID, + "intercept_header_auth": True, + } + if internal: + defaults["internal_host"] = internal + if unauth: + defaults["skip_path_regex"] = unauth + + provider, _ = ProxyProvider.objects.update_or_create(name=pname, defaults=defaults) + Application.objects.update_or_create( + slug=slug, + defaults={ + "name": aname, + "provider": provider, + "meta_launch_url": host, + "policy_engine_mode": "any", + }, + ) + providers.append(provider) + print(f"ok {slug} -> {host}") + +outpost = Outpost.objects.get(name="authentik Embedded Outpost") +outpost.type = OutpostType.PROXY +outpost.providers.set(providers) +outpost.save() +print(f"outpost providers: {outpost.providers.count()}") diff --git a/stacks/authentik/scripts/setup-gitea-oauth.py b/stacks/authentik/scripts/setup-gitea-oauth.py new file mode 100644 index 0000000..ea39511 --- /dev/null +++ b/stacks/authentik/scripts/setup-gitea-oauth.py @@ -0,0 +1,24 @@ +"""Register Authentik as Gitea OAuth source (run on host: docker exec gitea ...).""" +import subprocess +import sys + +CLIENT_ID = sys.argv[1] if len(sys.argv) > 1 else "" +CLIENT_SECRET = sys.argv[2] if len(sys.argv) > 2 else "" +if not CLIENT_ID or not CLIENT_SECRET: + print("usage: setup-gitea-oauth.py ") + sys.exit(1) + +DISCOVERY = "https://auth.ginnoir.com/application/o/gitea/.well-known/openid-configuration" + +cmd = [ + "docker", "exec", "-u", "git", "gitea", "gitea", "admin", "auth", "add-oauth", + "--name", "Authentik", + "--provider", "openidConnect", + "--key", CLIENT_ID, + "--secret", CLIENT_SECRET, + "--auto-discover-url", DISCOVERY, + "--scopes", "openid profile email", +] +print("running:", " ".join(cmd[:8]), "...") +subprocess.run(cmd, check=True) +print("gitea oauth source Authentik registered") diff --git a/stacks/authentik/scripts/setup-homarr-oidc.py b/stacks/authentik/scripts/setup-homarr-oidc.py new file mode 100644 index 0000000..b729c3a --- /dev/null +++ b/stacks/authentik/scripts/setup-homarr-oidc.py @@ -0,0 +1,47 @@ +"""Switch homarr from forward-auth proxy provider to native OIDC (run: ak shell -c exec(open(...).read())).""" +from authentik.core.models import Application +from authentik.flows.models import Flow +from authentik.outposts.models import Outpost +from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode +from authentik.providers.proxy.models import ProxyProvider + +AUTHZ = Flow.objects.get(slug="default-provider-authorization-implicit-consent") +INVALID = Flow.objects.get(slug="default-provider-invalidation-flow") + +REDIRECT = "https://homarr.ginnoir.com/api/auth/callback/oidc" + +provider, created = OAuth2Provider.objects.update_or_create( + name="homarr", + defaults={ + "authorization_flow": AUTHZ, + "invalidation_flow": INVALID, + "redirect_uris": [ + RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=REDIRECT), + ], + "access_code_validity": "minutes=1", + "access_token_validity": "hours=24", + "refresh_token_validity": "days=30", + }, +) +# Required for Homarr profile parsing — without these, userinfo lacks name/email claims. +default_mappings = OAuth2Provider.objects.get(name="famapp").property_mappings.all() +provider.property_mappings.set(default_mappings) +provider.save() +print(f"oauth2 provider homarr created={created} client_id={provider.client_id}") + +app = Application.objects.get(slug="homarr") +app.provider = provider +app.save() +print(f"application homarr -> OAuth2Provider homarr") + +# Drop homarr from the embedded proxy outpost (native OIDC replaces forward_auth here). +try: + proxy = ProxyProvider.objects.get(name="homelab-homarr") + outpost = Outpost.objects.get(name="authentik Embedded Outpost") + outpost.providers.remove(proxy) + print("removed homelab-homarr from embedded outpost") +except ProxyProvider.DoesNotExist: + print("homelab-homarr proxy provider not found (ok)") + +print(f"AUTH_OIDC_CLIENT_ID={provider.client_id}") +print(f"AUTH_OIDC_CLIENT_SECRET={provider.client_secret}") diff --git a/stacks/backup/init-kopia.sh b/stacks/backup/init-kopia.sh index 5115c06..4215a7c 100644 --- a/stacks/backup/init-kopia.sh +++ b/stacks/backup/init-kopia.sh @@ -44,7 +44,5 @@ else echo "init-kopia: B2 credentials empty — start UI only; configure repo manually (see README)" fi -# Web UI uses HTTP basic auth (distinct from repository encryption password). -exec kopia server start --ui --insecure --address=0.0.0.0:51515 \ - --server-username=kopia \ - --server-password="${KOPIA_PASSWORD}" +# Web UI auth is handled by Caddy forward_auth — no second login prompt. +exec kopia server start --ui --insecure --address=0.0.0.0:51515 --without-password diff --git a/stacks/dev/code-server.yaml b/stacks/dev/code-server.yaml new file mode 100644 index 0000000..5f0c9ec --- /dev/null +++ b/stacks/dev/code-server.yaml @@ -0,0 +1,3 @@ +bind-addr: 0.0.0.0:8443 +auth: none +cert: false diff --git a/stacks/dev/docker-compose.yml b/stacks/dev/docker-compose.yml index 9edcdc2..262dc99 100644 --- a/stacks/dev/docker-compose.yml +++ b/stacks/dev/docker-compose.yml @@ -82,11 +82,11 @@ services: - PUID=1000 - PGID=1000 - TZ=${TZ} - - PASSWORD=${CODE_SERVER_PASSWORD} - - SUDO_PASSWORD=${CODE_SERVER_PASSWORD} - DEFAULT_WORKSPACE=/workspace + # Authentik forward_auth at the edge — local password auth disabled via config mount. volumes: - /config/code-server:/config + - /config/code-server/code-server.yaml:/config/.config/code-server/config.yaml - /home/ginnoir:/workspace ports: - "8443:8443" @@ -146,6 +146,13 @@ services: - DB_USER=bookstack - DB_PASS=${BOOKSTACK_DB_PASSWORD} - DB_DATABASE=bookstack + - AUTH_METHOD=oidc + - AUTH_AUTO_INITIATE=true + - OIDC_NAME=Authentik + - OIDC_CLIENT_ID=${BOOKSTACK_OIDC_CLIENT_ID} + - OIDC_CLIENT_SECRET=${BOOKSTACK_OIDC_CLIENT_SECRET} + - OIDC_ISSUER=https://auth.ginnoir.com/application/o/bookstack/ + - OIDC_ISSUER_DISCOVER=true volumes: - /config/bookstack:/config ports: diff --git a/stacks/dev/stack.env b/stacks/dev/stack.env index 1877e85..efec476 100644 --- a/stacks/dev/stack.env +++ b/stacks/dev/stack.env @@ -12,6 +12,8 @@ CODE_SERVER_PASSWORD=c0de_s3rver_pass BOOKSTACK_APP_KEY=base64:cSR6L4TPEbM2IFi9MyZcnmexvzAlIHAjn/qMzQKO/t4= BOOKSTACK_DB_PASSWORD=bookstack_d3v_pass BOOKSTACK_DB_ROOT_PASSWORD=bookstack_r00t_pass +BOOKSTACK_OIDC_CLIENT_ID=8YV4BI2JVIG1QzS2DbylEgqv56sgfiOVRXOgw2Z5 +BOOKSTACK_OIDC_CLIENT_SECRET=wIyiw7LmGBORDyTdc2crSZryXJHzIq4gtajAKSpOU7e8BkBSO6SstM9gGPrnp4tbJhCFSQggYAgtYd5LOjUvQpSWInzm0JLlmLg1BTlPq5EVVieNYuWRCCQrerj8gRZ0 # Plane project management # Rotate with: openssl rand -hex 32 diff --git a/stacks/monitoring/docker-compose.yml b/stacks/monitoring/docker-compose.yml index 4d87c89..d06956b 100644 --- a/stacks/monitoring/docker-compose.yml +++ b/stacks/monitoring/docker-compose.yml @@ -21,6 +21,8 @@ services: networks: [edge] env_file: - stack.env + environment: + - BASE_URL=https://homarr.ginnoir.com ports: - "7575:7575" volumes: diff --git a/stacks/monitoring/stack.env b/stacks/monitoring/stack.env index 2699827..6939348 100644 --- a/stacks/monitoring/stack.env +++ b/stacks/monitoring/stack.env @@ -1,3 +1,19 @@ # monitoring stack secrets — injected via env_file; container-exact var names. # Homarr config encryption key. Generate with: openssl rand -hex 32 SECRET_ENCRYPTION_KEY=98989838bf06c9e42d77944ce848963a22ab316b55a09b0d199652ef0934394f +# Auth.js session/state signing — required for OIDC (openssl rand -base64 32) +AUTH_SECRET=7Kp2mN9xQvR4sT6wY8zA1bC3dE5fG0hJ2kL4nM6pS8uV0xZ= + +# Homarr native OIDC via Authentik (replaces forward_auth + credentials login) +AUTH_PROVIDERS=oidc +AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/homarr/ +AUTH_OIDC_CLIENT_ID=u5XiQcEenMoo3cViXOw3OWt644m2AMsbUXNpInqu +AUTH_OIDC_CLIENT_SECRET=PTnXULynOv7HreiqnCmAWOc9vy81gyA57NIS7rxJnBc8g6WeD7fo4d5kWoK54U3QodMrTXVAtT6GoTcqJznEONQDzeZXGe25fZAbcQNtOdgpLakoLKHP1aVU2jJ2nkOR +AUTH_OIDC_CLIENT_NAME=Authentik +AUTH_OIDC_AUTO_LOGIN=true +AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING=true +AUTH_OIDC_SCOPE_OVERWRITE=openid email profile +AUTH_OIDC_GROUPS_ATTRIBUTE=groups +AUTH_OIDC_FORCE_USERINFO=true +AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE=name +AUTH_LOGOUT_REDIRECT_URL=https://auth.ginnoir.com/application/o/homarr/end-session/ diff --git a/stacks/resume/docker-compose.yml b/stacks/resume/docker-compose.yml index f247cb0..32fd8d2 100644 --- a/stacks/resume/docker-compose.yml +++ b/stacks/resume/docker-compose.yml @@ -38,6 +38,12 @@ services: - /storage1/labdata/resume/minio:/data env_file: - stack.env + environment: + - MINIO_BROWSER_REDIRECT_URL=https://minio.ginnoir.com + - MINIO_IDENTITY_OPENID_CONFIG_URL=https://auth.ginnoir.com/application/o/minio/.well-known/openid-configuration + - MINIO_IDENTITY_OPENID_CLIENT_ID=${MINIO_OIDC_CLIENT_ID} + - MINIO_IDENTITY_OPENID_CLIENT_SECRET=${MINIO_OIDC_CLIENT_SECRET} + - MINIO_IDENTITY_OPENID_SCOPES=openid,profile,email healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] interval: 30s diff --git a/stacks/resume/stack.env b/stacks/resume/stack.env index 9350eee..e0ca2f9 100644 --- a/stacks/resume/stack.env +++ b/stacks/resume/stack.env @@ -9,6 +9,8 @@ POSTGRES_PASSWORD=postgres # minio (resume-minio service) MINIO_ROOT_USER=minioadmin MINIO_ROOT_PASSWORD=minioadmin +MINIO_OIDC_CLIENT_ID=1jC2ChsCjdh7srOKtr9Jzv1l2RF8omrzshEpAESa +MINIO_OIDC_CLIENT_SECRET=66KML3RcsdcStPO6FIe04cCRyLTztfUDdHcpkIVImUO6WTTHkAFtao4GfZYu9yq4NvFGWxztTv7M8uGZBi4xaVUtJr3K3GU47pLaaWwF1WMsidsRTy760QUJxsUq3i9C # browserless chrome auth token (chrome reads TOKEN, the app reads PRINTER_TOKEN) TOKEN=chrome_token