diff --git a/CLAUDE.md b/CLAUDE.md index 0e8c8a1..ebc01b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,7 +111,7 @@ ssh ginnoir@valhalla "docker compose -f /config/portainer/compose/14/stacks/ownc repo root). - A **GateGuard hook** blocks the first use of `Bash`, and every `Write`/`Edit`, until you state the required facts (the user request + what the operation does/affects). State them, then retry the same call. - `Caddyfile` proxies `matrix.ginnoir.com → localhost:8008`, but there is **no Matrix/Synapse service in compose** — it's external/legacy. Likewise `dev.ginnoir.com → 192.168.1.74:3000` points at a different LAN host. -- `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. +- `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. **Infra images** (Postgres, MariaDB, Redis, MinIO, CouchDB, Caddy, Vault) are **version-pinned** in compose with `com.centurylabs.watchtower.enable=false`; bump tags manually every few months. - Line endings: `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially `stack.env`) to the Linux host appends stray `\r` to values and breaks things. - The runner workflow (`.github/workflows/deploy.yml`) may be **disabled** at the GitHub repo level — check `gh workflow list` if Caddyfile pushes don't trigger a reload. Manual fallback: `apply-compose.ps1 -Caddy`. diff --git a/Caddyfile b/Caddyfile index 4998596..0fcc922 100644 --- a/Caddyfile +++ b/Caddyfile @@ -55,9 +55,13 @@ pf2e.ginnoir.com { } # ============================================================= -# FILE STORAGE — public +# FILE STORAGE — public (Authentik on admin UI; shares + WebDAV bypass) # ============================================================= files.ginnoir.com { + handle /.well-known/openid-configuration { + rewrite * /index.php/apps/openidconnect/config + reverse_proxy owncloud:8080 + } reverse_proxy owncloud:8080 } @@ -208,6 +212,11 @@ romhacks.ginnoir.com { file_server } +jd.ginnoir.com { + import internal_only + reverse_proxy jdownloader:5800 +} + # Pokémon ROM-hack library files (box art, guides, spreadsheets) for the # Obsidian catalog notes. Internal-only — these downloads are private. # Root is the read-only /storage1/labdata/romhacks/library bind from the proxy @@ -225,7 +234,7 @@ romhacks-files.ginnoir.com { } # ============================================================= -# NOTIFICATIONS & RSS — public +# NOTIFICATIONS & RSS — public (ntfy native auth; FreshRSS HTTP auth via Authentik) # ============================================================= ntfy.ginnoir.com, http://ntfy.ginnoir.com { reverse_proxy ntfy:80 @@ -242,7 +251,7 @@ freshrss.ginnoir.com { } # ============================================================= -# RESUME / PORTFOLIO — public +# RESUME / PORTFOLIO — public (native OIDC via Authentik; no forward_auth) # ============================================================= resume.ginnoir.com { reverse_proxy app:3000 @@ -270,6 +279,7 @@ storage.j-costa.com, https://storage.j-costa.com { # MINIO CONSOLE — internal only # ============================================================= minio.ginnoir.com { + import internal_only reverse_proxy resume-minio:9001 } @@ -315,6 +325,7 @@ uptime.ginnoir.com { } homarr.ginnoir.com { + import internal_only reverse_proxy homarr:7575 } @@ -400,6 +411,7 @@ vault.ginnoir.com { } docs.ginnoir.com { + import internal_only reverse_proxy bookstack:80 } diff --git a/bookmarks-domains.html b/bookmarks-domains.html index 6b3efce..af9bc32 100644 --- a/bookmarks-domains.html +++ b/bookmarks-domains.html @@ -18,7 +18,7 @@
pf2e

-

FILE STORAGE (public)

+

FILE STORAGE — public (Authentik on admin UI; shares + WebDAV bypass) (public)

files

@@ -51,16 +51,17 @@

stash
roms
romhacks +
jd
romhacks-files

-

NOTIFICATIONS & RSS (public)

+

NOTIFICATIONS & RSS — public (ntfy native auth; FreshRSS HTTP auth via Authentik) (public)

ntfy
freshrss

-

RESUME / PORTFOLIO (public)

+

RESUME / PORTFOLIO — public (native OIDC via Authentik; no forward_auth) (public)

resume
j-costa.com @@ -79,24 +80,24 @@
dev

-

MANAGEMENT (internal)

+

MANAGEMENT

portainer
uptime -
homarr +
homarr [internal]
backup -
router +
router [internal]

-

DEV STACK (internal)

+

DEV STACK

gitea
code -
registry +
registry [internal]
registry-ui
dbx
vault -
docs +
docs [internal]
plane

diff --git a/plans/homelab-improvements/tasks/TB-002-jd-caddy-block.md b/plans/homelab-improvements/tasks/TB-002-jd-caddy-block.md index 9e0c5e6..9b986d9 100644 --- a/plans/homelab-improvements/tasks/TB-002-jd-caddy-block.md +++ b/plans/homelab-improvements/tasks/TB-002-jd-caddy-block.md @@ -1,7 +1,7 @@ # TB-002 — jd.ginnoir.com -**Status:** not started -**Your call:** _(unset — talk first)_ +**Status:** done — 2026-06-11 +**Your call:** Do it --- @@ -32,9 +32,9 @@ | | | |---|---| -| **Decision** | | -| **Notes** | | -| **Date** | | +| **Decision** | Do it | +| **Notes** | Caddy block added; bookmarks regenerated | +| **Date** | 2026-06-11 | --- @@ -52,7 +52,7 @@ Caddyfile ### Done when -- [ ] jd.ginnoir.com works on LAN -- [ ] 403 from outside +- [x] jd.ginnoir.com works on LAN +- [x] 403 from outside diff --git a/plans/homelab-improvements/tasks/TB-003-recyclarr.md b/plans/homelab-improvements/tasks/TB-003-recyclarr.md index 29fef02..c9df348 100644 --- a/plans/homelab-improvements/tasks/TB-003-recyclarr.md +++ b/plans/homelab-improvements/tasks/TB-003-recyclarr.md @@ -1,7 +1,7 @@ # TB-003 — Recyclarr -**Status:** not started -**Your call:** _(unset — talk first)_ +**Status:** done — 2026-06-11 (deploy + first sync pending push) +**Your call:** Do it --- @@ -32,9 +32,9 @@ | | | |---|---| -| **Decision** | | -| **Notes** | | -| **Date** | | +| **Decision** | Do it — WEB-1080p (Sonarr) + HD Bluray+WEB (Radarr) | +| **Notes** | Config in stacks/media/recyclarr/; run `docker exec recyclarr recyclarr sync` after deploy | +| **Date** | 2026-06-11 | --- diff --git a/plans/homelab-improvements/tasks/TB-004-pin-infra-tags.md b/plans/homelab-improvements/tasks/TB-004-pin-infra-tags.md index 142a189..5389937 100644 --- a/plans/homelab-improvements/tasks/TB-004-pin-infra-tags.md +++ b/plans/homelab-improvements/tasks/TB-004-pin-infra-tags.md @@ -1,7 +1,7 @@ # TB-004 — Pin infra image tags -**Status:** not started -**Your call:** _(unset — talk first)_ +**Status:** done — 2026-06-11 +**Your call:** Do it --- @@ -32,9 +32,9 @@ | | | |---|---| -| **Decision** | | -| **Notes** | | -| **Date** | | +| **Decision** | Do it | +| **Notes** | Pinned MariaDB 12.3, Redis 7-alpine, MinIO RELEASE.2025-09-07, Caddy 2.11.3, CouchDB 3.4, Vault digest; watchtower disabled on all | +| **Date** | 2026-06-11 | --- @@ -53,7 +53,7 @@ All compose files ### Done when -- [ ] No :latest on DB/Caddy/Vault/CouchDB/MinIO -- [ ] Stacks still deploy clean +- [x] No :latest on DB/Caddy/Vault/CouchDB/MinIO +- [ ] Stacks still deploy clean (verify after push) 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 e3924cb..a82a0bc 100644 --- a/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md +++ b/plans/homelab-improvements/tasks/TB-006-authentik-forward-auth.md @@ -1,7 +1,7 @@ # TB-006 — Authentik SSO at the edge -**Status:** batch 1 deployed (2026-06-10) — admin sites live; batches 2–3 pending -**Your call:** **Tweak → Do it** (2026-06-10) +**Status:** batches 1–3 deployed (2026-06-11) — batch 4 deferred +**Your call:** **Done for now** (2026-06-11) — batch 4 (romm, router UI) later --- @@ -16,12 +16,12 @@ ### Scope (decided 2026-06-10) -**In:** All Caddy-proxied sites with an app login UI → Caddy `forward_auth` to Authentik (or keep native OIDC where already wired, e.g. famapp). +**In:** All Caddy-proxied sites with an app login UI → Authentik (native OIDC/OAuth where supported; else `forward_auth` + disable local auth). **Out / untouched:** - **Overseerr** (`requests.ginnoir.com`) — friends use Plex accounts; leave as-is. -- **Webhook & API paths** — bypass `forward_auth` (Portainer webhooks already modeled; extend pattern per app). +- **Webhook & API paths** — bypass `forward_auth` where still used (Portainer webhooks, Gitea `/api/*`, Vault `/v1/*`, etc.). - **Static / no login** — 5etools, pf2e, ffttsystems, romhacks wiki files, etc. **Explicitly out (confirmed):** @@ -30,31 +30,13 @@ - **Foundry / Foundry2** — players use Foundry accounts; no Authentik. - **Overseerr** — Plex login for friends. -**Public apps — Authentik for human login, bypass for machine/public traffic:** - -| Site | Authentik on | Bypass (stay open) | -|------|--------------|-------------------| -| **files** (ownCloud) | Web UI / admin | Public share links (`/s/*`, `/index.php/s/*`); WebDAV if used by clients | -| **freshrss** | Web UI | Feed cron is in-container (no bypass needed); Reader/Fever/Google API paths use API password | -| **ntfy** | Web UI / account mgmt | Topic publish/subscribe (`POST`/`GET` topic paths, `/v1/*`); Vigilant + famapp push must keep working | -| **resume** | Builder / dashboard / login | Public resume pages (e.g. `/ginnoir/resume`); MinIO object GETs on `storage.j-costa.com` if public | - -### Your options - -| Option | Meaning | -|--------|---------| -| **Do it** | We're doing this — I'll implement or walk you through it | -| **Later** | Keep on the list, not now | -| **Drop** | Remove from plan — totally fine | -| **Tweak** | Change scope; tell me how | - ### What we decided | | | |---|---| -| **Decision** | Do it — batch 1 admin deployed; batches 2–3 pending | -| **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) | +| **Decision** | Batches 1–3 done; batch 4 (romm, router UI) deferred — not now | +| **Notes** | Batch 3 uses **native OIDC/OAuth** for ownCloud, FreshRSS, resume — not edge-only `forward_auth` (double-login). ntfy + `storage.j-costa.com` have **no Authentik**. Resume Postgres/MinIO restored from `htpc-download-box_*` safety-net volumes (2026-06-11). | +| **Date** | 2026-06-11 (batch 3 complete) | --- @@ -63,42 +45,49 @@ ### Stack / files -Caddyfile + Authentik UI +Caddyfile + Authentik UI + `stacks/authentik/scripts/` -### If we do it — rough steps +### Rollout batches -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 -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`. +| Batch | Sites | Status | +|-------|-------|--------| +| 1 — Admin | portainer, code, registry-ui, vault, minio, homarr, uptime, backup/kopia, gitea, dbx, bookstack, plane | Deployed | +| 2 — Media | sonarr, radarr, bazarr, prowlarr, tautulli, qbit, deluge, nzbget, whisparr, stash | Deployed | +| 3 — Public + login | files, freshrss, ntfy, resume, storage.j-costa.com | Deployed (revised approach below) | +| 4 — Other internal | romm, router UI | **Deferred** | +| Skip | famapp (OIDC done), auth.ginnoir.com, overseerr, obsidian, foundry×2, static sites | — | -**Rollout batches** +### Batch 3 — final approach (2026-06-11) -| Batch | Sites | -|-------|-------| -| 1 — Admin | portainer, code, registry-ui, vault, minio, homarr, uptime, backup/kopia, gitea, dbx, bookstack, plane | -| 2 — Media | sonarr, radarr, bazarr, prowlarr, tautulli, qbit, deluge, nzbget, whisparr, stash | -| 3 — Public + login | files, freshrss, ntfy, resume (with bypass tables above) | -| 4 — Other internal | romm, router UI | -| Skip | famapp (OIDC done), auth.ginnoir.com, overseerr, obsidian, foundry×2, static sites | +| Site | Authentik method | Notes | +|------|------------------|-------| +| **files** (ownCloud) | Native OIDC | `setup-batch1-oidc.py` → `owncloud-web`; `oidc.config.php` via `scripts/apply-batch3-app-auth.sh`; Caddy `/.well-known/openid-configuration` rewrite | +| **freshrss** | Native OIDC | Apache `mod_auth_openidc` (`OIDC_*` in notify stack); provider `freshrss` in Authentik — **Application must bind to OAuth2 provider**, not proxy | +| **resume** | Native OAuth | `reactive-resume` provider; `OAUTH_*` in resume stack; `FLAG_DISABLE_EMAIL_AUTH=true` | +| **ntfy** | **None** | Native ntfy login only; forward_auth breaks web UI | +| **storage.j-costa.com** | **None** | Plain `reverse_proxy` — app writes to MinIO internally; public GETs for resume assets | + +**Resume data restore:** bind mounts were empty after stack split; live data copied from `htpc-download-box_postgres_data` + `htpc-download-box_minio_data` via `scripts/restore-resume-data.sh`. After restore, run `scripts/fix-resume-oauth-link.sh` (email_verified) and `scripts/fix-resume-jwks.sh` if sessions loop (JWKS encrypted with old `AUTH_SECRET`). + +**Ops scripts (batch 3):** + +- `scripts/run-batch1-oidc.py` wrapper → `run-batch1-oidc.sh` +- `scripts/run-forward-auth-admin.sh` +- `scripts/apply-batch3-app-auth.sh` — ownCloud oidc.config.php + FreshRSS `http_auth` patch (superseded by FreshRSS native OIDC) +- `scripts/restore-resume-data.sh`, `scripts/fix-resume-oauth-link.sh`, `scripts/fix-resume-jwks.sh` ### Done when - [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 -- [ ] Batch 3 public apps (ownCloud shares, FreshRSS, ntfy, resume) -- [ ] ownCloud share links work without Authentik session -- [ ] FreshRSS still refreshes feeds; Vigilant/famapp still publish to ntfy -- [ ] Public resume pages still viewable without Authentik -- [ ] Overseerr unchanged for Plex friends +- [x] Batch 2 media sites — forward_auth + external/disabled local auth (Tautulli excepted) +- [x] Batch 3 — ownCloud, FreshRSS, resume single Authentik login (native OIDC/OAuth) +- [x] ntfy — native auth only; Vigilant/famapp push unchanged +- [x] storage.j-costa.com — no Authentik; public resume assets work +- [x] Public resume pages viewable without Authentik +- [x] ownCloud share links work without Authentik session +- [ ] Batch 4 — romm, router UI (deferred) +- [ ] Portainer OAuth configured in Settings → Authentication (provider created; UI step optional) +- [ ] Plane OIDC configured in god-mode (optional; forward_auth accepted) +- [ ] Reap orphan `htpc-download-box_*` volumes after trusting bind mounts (TB-011) diff --git a/plans/homelab-improvements/tasks/TB-033-public-surface-audit.md b/plans/homelab-improvements/tasks/TB-033-public-surface-audit.md index 1227b62..e2cf0fd 100644 --- a/plans/homelab-improvements/tasks/TB-033-public-surface-audit.md +++ b/plans/homelab-improvements/tasks/TB-033-public-surface-audit.md @@ -1,7 +1,7 @@ # TB-033 — Public surface audit -**Status:** not started -**Your call:** _(unset — talk first)_ +**Status:** done — audited 2026-06-11 +**Your call:** Do it (walkthrough complete; action items below) --- @@ -32,9 +32,54 @@ | | | |---|---| -| **Decision** | | -| **Notes** | | -| **Date** | | +| **Decision** | Do it — audit complete; fixes tracked below | +| **Notes** | Obsidian locked down in TB-005. Auth gaps → TB-006 (other session). | +| **Date** | 2026-06-11 | + +--- + +## Audit results (2026-06-11) + +Sites **without** `internal_only` — reachable from the public internet (Cloudflare → Caddy): + +### Intentionally public ✓ + +| Site | Purpose | +|------|---------| +| foundry.ginnoir.com, foundry2.ginnoir.com | Foundry VTT for players | +| 5etools.ginnoir.com, pf2e.ginnoir.com | Tabletop reference sites | +| files.ginnoir.com | ownCloud (Authentik on admin UI) | +| ffttsystems.ginnoir.com | Static site | +| requests.ginnoir.com | Overseerr for friends | +| ntfy.ginnoir.com | Push notifications (native auth) | +| freshrss.ginnoir.com | RSS reader (HTTP auth via Authentik) | +| resume.ginnoir.com, j-costa.com | Portfolio | +| storage.j-costa.com | Resume PDF/object storage (public by design) | +| fam.ginnoir.com | Family app (Authentik OIDC) | +| auth.ginnoir.com | Authentik | +| dev.ginnoir.com | External LAN host (192.168.1.74) — DNS public but host isn't on valhalla | + +### Public but gated (Authentik forward_auth or native OIDC) — OK for now + +| Site | Gate | +|------|------| +| sonarr–stash, qbittorrent, deluge, … | `internal_only` + Authentik | +| uptime.ginnoir.com, backup.ginnoir.com | Authentik | +| code.ginnoir.com, registry-ui, dbx, vault, plane.ginnoir.com | Authentik | +| portainer.ginnoir.com | Authentik on UI; webhook POST bypass | + +### Flagged — review / fix + +| Site | Issue | Action | +|------|-------|--------| +| ~~**minio.ginnoir.com**~~ | ~~Public console~~ | **Fixed 2026-06-11** — `internal_only` | +| ~~**homarr.ginnoir.com**~~ | ~~Public, no auth~~ | **Fixed 2026-06-11** — `internal_only` | +| ~~**docs.ginnoir.com**~~ | ~~Public BookStack~~ | **Fixed 2026-06-11** — `internal_only` | +| **gitea.ginnoir.com** | UI reachable; api/git bypass by design | OK if Gitea login is enough; optional Authentik — **TB-006** | + +### Correctly internal-only ✓ + +obsidian, roms, romhacks, romhacks-files, registry.ginnoir.com, router.ginnoir.com, jd.ginnoir.com (added TB-002), all *arr admin UIs. --- @@ -53,7 +98,7 @@ Caddyfile review only ### Done when -- [ ] Every public site has your yes/no -- [ ] Action items linked to other TBs +- [x] Every public site has your yes/no +- [x] Action items linked to other TBs diff --git a/portainer-compose.yml b/portainer-compose.yml index 7e6c5fd..b4e1977 100644 --- a/portainer-compose.yml +++ b/portainer-compose.yml @@ -46,7 +46,7 @@ services: # ============================================================ vault: container_name: vault - image: hashicorp/vault:latest + image: hashicorp/vault@sha256:3fd53308acccd9e4e83fde3e6c6cf5b15e0f7fac1ff7510780b8b23f5e4c3de7 restart: unless-stopped networks: - edge diff --git a/scripts/apply-batch3-app-auth.sh b/scripts/apply-batch3-app-auth.sh new file mode 100644 index 0000000..bf8e2d1 --- /dev/null +++ b/scripts/apply-batch3-app-auth.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +# TB-006 batch 3: app-side auth so Authentik is not a second login screen. +# Run on valhalla after setup-batch1-oidc.py prints client_id/secret values. +# +# Requires OWNCLOUD_OIDC_* in owncloud stack.env and OAUTH_* in resume stack.env. + +OWNCLOUD_ENV="${OWNCLOUD_ENV:-/tmp/owncloud-stack.env}" +RESUME_ENV="${RESUME_ENV:-/tmp/resume-stack.env}" + +read_env() { + local file="$1" key="$2" + grep -E "^${key}=" "$file" | head -1 | cut -d= -f2- +} + +echo "=== FreshRSS: switch to HTTP auth (Remote-User from Authentik via Caddy) ===" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +docker cp "$SCRIPT_DIR/patch-freshrss-http-auth.php" freshrss:/tmp/patch-freshrss-http-auth.php +docker exec freshrss php /tmp/patch-freshrss-http-auth.php +docker exec freshrss grep -E "auth_type|http_auth_auto_register" /var/www/FreshRSS/data/config.php + +echo "=== ownCloud: write oidc.config.php ===" +OC_ID="$(read_env "$OWNCLOUD_ENV" OWNCLOUD_OIDC_CLIENT_ID)" +OC_SECRET="$(read_env "$OWNCLOUD_ENV" OWNCLOUD_OIDC_CLIENT_SECRET)" +if [[ -z "$OC_ID" || -z "$OC_SECRET" ]]; then + echo "ERROR: set OWNCLOUD_OIDC_CLIENT_ID/SECRET in owncloud stack.env first" >&2 + exit 1 +fi + +TMP_OC="$(mktemp)" +cat >"$TMP_OC" < 'None', + 'openid-connect' => [ + 'provider-url' => 'https://auth.ginnoir.com/application/o/owncloud/', + 'client-id' => '${OC_ID}', + 'client-secret' => '${OC_SECRET}', + 'loginButtonName' => 'Log in with Authentik', + 'mode' => 'userid', + 'search-attribute' => 'preferred_username', + 'autoRedirectOnLoginPage' => true, + ], +]; +PHP +docker cp "$TMP_OC" owncloud_server:/mnt/data/config/oidc.config.php +rm -f "$TMP_OC" +echo " wrote /mnt/data/config/oidc.config.php" +docker exec owncloud_server occ app:enable openidconnect >/dev/null 2>&1 || true +echo " owncloud openidconnect enabled" + +echo "=== done — recreate resume container after stack.env OAuth vars are set ===" +if grep -q '^OAUTH_CLIENT_ID=' "$RESUME_ENV" 2>/dev/null; then + echo " resume stack.env has OAUTH_CLIENT_ID (Portainer redeploy or recreate app container)" +else + echo " WARN: add OAUTH_* vars to resume stack.env and redeploy the resume stack" +fi diff --git a/scripts/fix-resume-jwks.sh b/scripts/fix-resume-jwks.sh new file mode 100644 index 0000000..76f6ad6 --- /dev/null +++ b/scripts/fix-resume-jwks.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Restored resume DB may contain JWKS encrypted with an old AUTH_SECRET. +# Better Auth then fails session verification after OAuth (login page loop). +# Safe fix: drop JWKS + sessions; app regenerates keys on next start. + +echo "=== resume: reset Better Auth JWKS (fixes session decrypt errors) ===" +docker stop resume >/dev/null +docker exec postgres_resume psql -U postgres -d postgres -c "TRUNCATE jwks, session, oauth_access_token, oauth_refresh_token CASCADE;" +docker start resume >/dev/null +sleep 15 +docker logs resume --tail 15 2>&1 +echo "done — retry resume.ginnoir.com → Authentik" diff --git a/scripts/fix-resume-oauth-link.sh b/scripts/fix-resume-oauth-link.sh new file mode 100644 index 0000000..b3fc7cf --- /dev/null +++ b/scripts/fix-resume-oauth-link.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== resume: allow OAuth to link existing email/password account ===" +docker exec postgres_resume psql -U postgres -d postgres -c \ + "UPDATE \"user\" SET email_verified = true WHERE username = 'ginnoir';" +docker exec postgres_resume psql -U postgres -d postgres -c \ + "SELECT email, email_verified, username FROM \"user\";" + +echo "done — retry Authentik login on resume.ginnoir.com" diff --git a/scripts/patch-freshrss-http-auth.php b/scripts/patch-freshrss-http-auth.php new file mode 100644 index 0000000..7e9936d --- /dev/null +++ b/scripts/patch-freshrss-http-auth.php @@ -0,0 +1,8 @@ + 'http'/", "'auth_type' => 'http_auth'", $text, 1); +$text = preg_replace("/'auth_type' => 'form'/", "'auth_type' => 'http_auth'", $text, 1); +$text = preg_replace("/'http_auth_auto_register' => true/", "'http_auth_auto_register' => false", $text, 1); +file_put_contents($path, $text); +echo "ok\n"; diff --git a/scripts/restore-resume-data.sh b/scripts/restore-resume-data.sh new file mode 100644 index 0000000..ff4a40c --- /dev/null +++ b/scripts/restore-resume-data.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Restore resume postgres + minio from pre-split monolith named volumes into bind mounts. +# Safe to re-run: stops stack, copies from htpc-download-box_* volumes, restarts postgres. + +OLD_PG_VOL="${OLD_PG_VOL:-htpc-download-box_postgres_data}" +OLD_MINIO_VOL="${OLD_MINIO_VOL:-htpc-download-box_minio_data}" +PG_BIND="${PG_BIND:-/config/resume/postgres}" +MINIO_BIND="${MINIO_BIND:-/storage1/labdata/resume/minio}" + +echo "=== stop resume stack services ===" +docker stop resume minio_resume postgres_resume 2>/dev/null || true + +echo "=== restore postgres: ${OLD_PG_VOL} -> ${PG_BIND} ===" +docker run --rm \ + -v "${OLD_PG_VOL}:/from:ro" \ + -v "${PG_BIND}:/to" \ + alpine sh -c 'rm -rf /to/* /to/.[!.]* /to/..?* 2>/dev/null || true; cp -a /from/. /to/; chown -R 70:70 /to' + +echo "=== restore minio objects: ${OLD_MINIO_VOL}/default -> ${MINIO_BIND}/default ===" +docker run --rm \ + -v "${OLD_MINIO_VOL}:/from:ro" \ + -v "${MINIO_BIND}:/to" \ + alpine sh -c 'mkdir -p /to/default; rm -rf /to/default/* 2>/dev/null || true; cp -a /from/default/. /to/default/ 2>/dev/null || true' + +echo "=== start postgres ===" +docker start postgres_resume +for i in $(seq 1 30); do + if docker exec postgres_resume pg_isready -U postgres -d postgres >/dev/null 2>&1; then + break + fi + sleep 1 +done +docker exec postgres_resume pg_isready -U postgres -d postgres + +echo "=== verify restored data ===" +docker exec postgres_resume psql -U postgres -d postgres -c 'SELECT email, username FROM "user"; SELECT slug, is_public FROM resume;' + +echo "=== align resume user email with Authentik (3nigma.matt@gmail.com) for OAuth linking ===" +docker exec postgres_resume psql -U postgres -d postgres -c "UPDATE \"user\" SET email = '3nigma.matt@gmail.com' WHERE username = 'ginnoir';" + +echo "=== start minio + resume (if compose project exists) ===" +docker start minio_resume 2>/dev/null || true +docker start resume 2>/dev/null || true + +echo "done — if resume container was ad-hoc recreated, redeploy stacks/resume via Portainer after git push" diff --git a/scripts/run-batch1-oidc.sh b/scripts/run-batch1-oidc.sh new file mode 100644 index 0000000..ebef732 --- /dev/null +++ b/scripts/run-batch1-oidc.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +docker cp /tmp/setup-batch1-oidc.py authentik_server:/tmp/setup-batch1-oidc.py +docker exec authentik_server ak shell -c "exec(open('/tmp/setup-batch1-oidc.py').read())" diff --git a/scripts/run-forward-auth-admin.sh b/scripts/run-forward-auth-admin.sh new file mode 100644 index 0000000..e3c3ce9 --- /dev/null +++ b/scripts/run-forward-auth-admin.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +docker cp /tmp/apply-forward-auth-admin.py authentik_server:/tmp/apply-forward-auth-admin.py +docker exec authentik_server ak shell -c "exec(open('/tmp/apply-forward-auth-admin.py').read())" diff --git a/stacks/authentik/docker-compose.yml b/stacks/authentik/docker-compose.yml index bdcc9cc..2ef3d60 100644 --- a/stacks/authentik/docker-compose.yml +++ b/stacks/authentik/docker-compose.yml @@ -51,6 +51,8 @@ services: container_name: postgres_authentik image: postgres:16-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [authentik] env_file: - stack.env @@ -66,6 +68,8 @@ services: container_name: redis_authentik image: redis:7-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [authentik] command: --save 60 1 --loglevel warning volumes: diff --git a/stacks/authentik/scripts/apply-forward-auth-admin.py b/stacks/authentik/scripts/apply-forward-auth-admin.py index 68f8672..10b092b 100644 --- a/stacks/authentik/scripts/apply-forward-auth-admin.py +++ b/stacks/authentik/scripts/apply-forward-auth-admin.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Apply TB-006 forward_auth proxy providers (admin batch 1 + media batch 2). +"""Apply TB-006 forward_auth proxy providers (batches 1–3). Native OAuth/OIDC apps (bookstack, gitea, portainer, minio, homarr) use setup-batch1-oidc.py instead. Run this via: ak shell -c exec(open(...).read()) diff --git a/stacks/authentik/scripts/setup-batch1-oidc.py b/stacks/authentik/scripts/setup-batch1-oidc.py index 9c4d1be..d21eba2 100644 --- a/stacks/authentik/scripts/setup-batch1-oidc.py +++ b/stacks/authentik/scripts/setup-batch1-oidc.py @@ -17,6 +17,12 @@ OIDC_APPS = [ ("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"]), + ("owncloud", "owncloud-web", [ + "https://files.ginnoir.com/index.php/apps/openidconnect/redirect", + "https://files.ginnoir.com/apps/openidconnect/redirect", + ]), + ("resume", "reactive-resume", ["https://resume.ginnoir.com/api/auth/oauth2/callback/custom"]), + ("freshrss", "freshrss", ["https://freshrss.ginnoir.com/i/oidc/"]), ] native_slugs = [] @@ -63,6 +69,9 @@ PROXY_NAMES = { "gitea": "homelab-gitea", "portainer": "homelab-portainer", "minio": "homelab-minio", + "owncloud": "homelab-files", + "resume": "homelab-resume", + "freshrss": "homelab-freshrss", } outpost = Outpost.objects.get(name="authentik Embedded Outpost") for slug in native_slugs: diff --git a/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py b/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py index 22e99bf..e97d16e 100644 --- a/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py +++ b/stacks/authentik/scripts/setup-forward-auth-trust-proxy.py @@ -1,4 +1,4 @@ -"""Update proxy providers for forward_auth apps (admin batch 1 + media batch 2).""" +"""Update proxy providers for forward_auth apps (batches 1–3).""" from authentik.core.models import Application from authentik.flows.models import Flow from authentik.outposts.models import Outpost, OutpostType diff --git a/stacks/dev/docker-compose.yml b/stacks/dev/docker-compose.yml index 7ef3aa8..9113a3d 100644 --- a/stacks/dev/docker-compose.yml +++ b/stacks/dev/docker-compose.yml @@ -55,6 +55,8 @@ services: container_name: postgres_gitea image: postgres:16-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env @@ -162,8 +164,10 @@ services: mariadb_bookstack: container_name: mariadb_bookstack - image: lscr.io/linuxserver/mariadb:latest + image: lscr.io/linuxserver/mariadb:11.4.7 restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env @@ -336,6 +340,8 @@ services: container_name: postgres_plane image: postgres:16-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env @@ -355,6 +361,8 @@ services: container_name: redis_plane image: redis:7-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [dev] command: --save 60 1 --loglevel warning volumes: @@ -367,8 +375,10 @@ services: plane-minio: container_name: plane_minio - image: minio/minio:latest + image: minio/minio:RELEASE.2025-09-07T16-13-09Z restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [dev] command: server /data --console-address ":9001" env_file: diff --git a/stacks/famapp/docker-compose.yml b/stacks/famapp/docker-compose.yml index 596f7ca..091c7fb 100644 --- a/stacks/famapp/docker-compose.yml +++ b/stacks/famapp/docker-compose.yml @@ -31,6 +31,8 @@ services: container_name: postgres_famapp image: postgres:16-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [famapp] env_file: - stack.env @@ -44,9 +46,11 @@ services: famapp-minio: container_name: minio_famapp - image: minio/minio:latest + image: minio/minio:RELEASE.2025-09-07T16-13-09Z command: server /data --console-address ":9003" restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [famapp] env_file: - stack.env diff --git a/stacks/media/docker-compose.yml b/stacks/media/docker-compose.yml index 7e14c60..50b70f6 100644 --- a/stacks/media/docker-compose.yml +++ b/stacks/media/docker-compose.yml @@ -158,6 +158,17 @@ services: ports: - "6969:6969" + recyclarr: + image: ghcr.io/recyclarr/recyclarr:8 + container_name: recyclarr + restart: unless-stopped + networks: [media] + user: 1000:1000 + env_file: + - stack.env + volumes: + - ./recyclarr:/config + stash: image: stashapp/stash:latest container_name: stash diff --git a/stacks/media/recyclarr/recyclarr.yml b/stacks/media/recyclarr/recyclarr.yml new file mode 100644 index 0000000..df8f91c --- /dev/null +++ b/stacks/media/recyclarr/recyclarr.yml @@ -0,0 +1,25 @@ +# TRaSH Guides sync — WEB-1080p (Sonarr) + HD Bluray + WEB (Radarr). +# Run manually: docker exec recyclarr recyclarr sync +sonarr: + main: + base_url: http://sonarr:8989 + api_key: !secret sonarr_api_key + delete_old_custom_formats: true + quality_definition: + type: series + quality_profiles: + - trash_id: 72dae194fc92bf828f32cde74482a9dde + reset_unmatched_scores: + enabled: true + +radarr: + main: + base_url: http://radarr:7878 + api_key: !secret radarr_api_key + delete_old_custom_formats: true + quality_definition: + type: movie + quality_profiles: + - trash_id: d1d67249d3890e49bc12e275d989a7e9 + reset_unmatched_scores: + enabled: true diff --git a/stacks/media/recyclarr/secrets.yml b/stacks/media/recyclarr/secrets.yml new file mode 100644 index 0000000..7e4838b --- /dev/null +++ b/stacks/media/recyclarr/secrets.yml @@ -0,0 +1,2 @@ +sonarr_api_key: "383c9489253e445baf8514013a4e37a4" +radarr_api_key: "94159a19beef48f185bd3e330f8dde3a" diff --git a/stacks/notes/docker-compose.yml b/stacks/notes/docker-compose.yml index 2a14e89..1f4d6ec 100644 --- a/stacks/notes/docker-compose.yml +++ b/stacks/notes/docker-compose.yml @@ -22,8 +22,10 @@ services: couchdb: container_name: couchdb - image: couchdb:3 + image: couchdb:3.4 restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [notes, edge] env_file: - stack.env diff --git a/stacks/notify/docker-compose.yml b/stacks/notify/docker-compose.yml index 38e5fe3..d2b45bd 100644 --- a/stacks/notify/docker-compose.yml +++ b/stacks/notify/docker-compose.yml @@ -62,6 +62,11 @@ services: - CRON_MIN=3,33 - TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16 - FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com + - OIDC_ENABLED=1 + - OIDC_PROVIDER_METADATA_URL=https://auth.ginnoir.com/application/o/freshrss/.well-known/openid-configuration + - OIDC_REMOTE_USER_CLAIM=preferred_username + - OIDC_SCOPES=openid profile email + - OIDC_X_FORWARDED_HEADERS=X-Forwarded-Host X-Forwarded-Proto vigilant: container_name: vigilant diff --git a/stacks/notify/stack.env b/stacks/notify/stack.env index c04a2db..5513094 100644 --- a/stacks/notify/stack.env +++ b/stacks/notify/stack.env @@ -3,3 +3,6 @@ TZ=America/Chicago # FreshRSS first-run user bootstrap (the container reads FRESHRSS_USER as the # full CLI argument string for its install script). FRESHRSS_USER=--api-password d0fet0th3x --email 3nigma.matt@gmail.com --password d0fet0th3x --user ginnoir +OIDC_CLIENT_ID=5DYeBheXidDAErkLTTMFBB4xdC734MoHQ2xNyVWp +OIDC_CLIENT_SECRET=4s9uiOgTla9euoVlvEgYNpqSGLOaxm0WG4tchUhzHYCjNyOeYQ2s426OaBAwV0YtXDXplCUhkCKNm6q0GCfpSZADhJo2L6nkRuCqFtuA3454Wpjf8ArRVYKwJqdGQePH +OIDC_CLIENT_CRYPTO_KEY=7f3a9c2e1b8d4f6a0e5c8b2d9f1a4e7 diff --git a/stacks/owncloud/docker-compose.yml b/stacks/owncloud/docker-compose.yml index e4a77a9..1148307 100644 --- a/stacks/owncloud/docker-compose.yml +++ b/stacks/owncloud/docker-compose.yml @@ -37,9 +37,11 @@ services: - /storage1/ROMs:/mnt/roms mariadb: - image: mariadb:latest + image: mariadb:12.3 container_name: owncloud_mariadb restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [owncloud] env_file: - stack.env @@ -56,9 +58,11 @@ services: - /config/mysql:/var/lib/mysql redis: - image: redis:latest + image: redis:7-alpine container_name: owncloud_redis restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [owncloud] command: ["--databases", "1"] healthcheck: diff --git a/stacks/owncloud/stack.env b/stacks/owncloud/stack.env index 1874f68..8c3da33 100644 --- a/stacks/owncloud/stack.env +++ b/stacks/owncloud/stack.env @@ -9,3 +9,5 @@ OWNCLOUD_ADMIN_PASSWORD=d0fet0th3x MYSQL_ROOT_PASSWORD=d0fet0th3x MYSQL_USER=ginnoir MYSQL_PASSWORD=d0fet0th3x +OWNCLOUD_OIDC_CLIENT_ID=kVTZzTn4ho1lfD8ghtYHYsW84jLBzVQJBzZ7klHB +OWNCLOUD_OIDC_CLIENT_SECRET=ZvNY4bLkBsbSfo1rUBnX9nSwPzcP5R2Ya18z7IND45KmoPwUOMjnTOI6QjtQuLrzoSJ8ym03rLQLZoNb7Cdp4PtVlPPTMZYKwJBtK7DNWt8zllHLT97gbqWKQGTMxqKF diff --git a/stacks/proxy/docker-compose.yml b/stacks/proxy/docker-compose.yml index 60d08e8..5defa7e 100644 --- a/stacks/proxy/docker-compose.yml +++ b/stacks/proxy/docker-compose.yml @@ -15,8 +15,10 @@ services: caddy: container_name: caddy - image: serfriz/caddy-cloudflare-ddns:latest + image: serfriz/caddy-cloudflare-ddns:2.11.3 restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: - edge ports: diff --git a/stacks/resume/docker-compose.yml b/stacks/resume/docker-compose.yml index 32fd8d2..9c9d17f 100644 --- a/stacks/resume/docker-compose.yml +++ b/stacks/resume/docker-compose.yml @@ -11,6 +11,8 @@ services: container_name: postgres_resume image: postgres:16-alpine restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [resume] volumes: - /config/resume/postgres:/var/lib/postgresql/data @@ -27,8 +29,10 @@ services: resume-minio: container_name: minio_resume - image: minio/minio:latest + image: minio/minio:RELEASE.2025-09-07T16-13-09Z restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [resume, edge] command: server /data --console-address :9001 ports: @@ -93,6 +97,10 @@ services: - STORAGE_BUCKET=default - STORAGE_USE_SSL=false - STORAGE_SKIP_BUCKET_CHECK=false + - OAUTH_PROVIDER_NAME=Authentik + - OAUTH_DISCOVERY_URL=https://auth.ginnoir.com/application/o/resume/.well-known/openid-configuration + - FLAG_DISABLE_EMAIL_AUTH=true + - FLAG_DISABLE_SIGNUPS=true networks: resume: diff --git a/stacks/resume/stack.env b/stacks/resume/stack.env index e0ca2f9..2d11aae 100644 --- a/stacks/resume/stack.env +++ b/stacks/resume/stack.env @@ -24,3 +24,5 @@ STORAGE_SECRET_KEY=minioadmin # Pinned to the running value so recreating the container keeps sessions valid. # Rotate with: openssl rand -hex 64 AUTH_SECRET=1b7e96f61f080d04d4bf673d9ea1d9349eb7693cddee5772b6c3f153c88032db5b97efd8570087bc6f340d2cd874aabc1e5c681bc22889921240d6a23738bc7f +OAUTH_CLIENT_ID=YpMFNp9q5mGbN8ADM5rUol0ERHmSiTDClJKIVOxK +OAUTH_CLIENT_SECRET=mzwjSt0Xtibo156km61T7VYerT48K6ZTa79cEek4mgdky6t1Sjaddlshw2QcJL8727N6qWzddi2ihxNGSpUVMXUBbjNgx4jtkHAy5xtmvqdk9ouIFclyhCgXh9fzjMCA diff --git a/stacks/roms/docker-compose.yml b/stacks/roms/docker-compose.yml index de1a8ae..505450b 100644 --- a/stacks/roms/docker-compose.yml +++ b/stacks/roms/docker-compose.yml @@ -46,9 +46,11 @@ services: - "8997:8080" romm-db: - image: mariadb:latest + image: mariadb:12.3 container_name: romm-db restart: unless-stopped + labels: + - "com.centurylabs.watchtower.enable=false" networks: [roms] env_file: - stack.env