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 @@
-
@@ -51,16 +51,17 @@
-
-
-
-
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" <