# ============================================================= # GLOBAL OPTIONS # ============================================================= { acme_dns cloudflare {env.CF_API_TOKEN} } # ============================================================= # SNIPPETS # ============================================================= # Reusable matcher — blocks anything not on the LAN or tailnet. # Usage: import internal_only inside any site block. # 100.64.0.0/10 is the Tailscale CGNAT range — allows tailnet clients that # reach Caddy without subnet-route SNAT (i.e. --snat-subnet-routes=false). (internal_only) { @blocked not remote_ip 192.168.1.0/24 172.16.0.0/12 100.64.0.0/10 127.0.0.1 respond @blocked "Access denied" 403 } # TB-006 — Authentik forward auth (embedded outpost on authentik-server:9000). # Wrap protected sites in `route { ... }` so bypass handles run before forward_auth. (authentik_outpost) { reverse_proxy /outpost.goauthentik.io/* authentik-server:9000 } (authentik_forward_auth) { forward_auth authentik-server:9000 { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version trusted_proxies private_ranges } } # Clients on LAN, Docker, or tailnet (same ranges as internal_only). (remote_internal) { remote_ip 192.168.1.0/24 172.16.0.0/12 100.64.0.0/10 127.0.0.1 } # Shared-link gate — external users visit ?k= once to get a 30-day cookie. # Internal IPs and tailnet clients pass through unconditionally. # Usage: import share_gate (replaces import internal_only on shared services) # Uses {$SHARE_KEY} (parse-time substitution) — baked in at caddy reload/start. # SameSite=Lax (not Strict): Strict causes Safari/Firefox to drop the cookie on the # redirect when the top-level navigation originated from an external app (Discord). # Rotate: update SHARE_KEY in stack.env, push → Portainer redeploys → new key. (share_gate) { @external not remote_ip 192.168.1.0/24 172.16.0.0/12 100.64.0.0/10 127.0.0.1 handle @external { route { @has_key query k={$SHARE_KEY} handle @has_key { header Set-Cookie "share_access={$SHARE_KEY}; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax" redir {http.request.uri.path} 302 } @has_cookie expression `{http.request.cookie.share_access} == "{$SHARE_KEY}"` handle @has_cookie { } handle { respond "Access denied" 403 } } } } # ============================================================= # FOUNDRY VTT — public # ============================================================= foundry.ginnoir.com { reverse_proxy foundry:30000 } foundry2.ginnoir.com { reverse_proxy foundry2:30000 } # ============================================================= # TABLETOP TOOLS — public # ============================================================= 5etools.ginnoir.com { reverse_proxy 5etools:80 } # ============================================================= # FILE STORAGE — internal only (Nextcloud) # ============================================================= files.ginnoir.com { import internal_only redir /.well-known/carddav /remote.php/dav 301 redir /.well-known/caldav /remote.php/dav 301 reverse_proxy nextcloud:80 { header_up X-Forwarded-Proto https header_up X-Real-IP {remote_host} } } # ============================================================= # STATIC SITES — public # ============================================================= wa4.ginnoir.com { root * /srv/wa4 file_server } # ============================================================= # MEDIA REQUESTS — public # ============================================================= requests.ginnoir.com { reverse_proxy seerr:5055 } # ============================================================= # MEDIA MANAGEMENT — internal only # ============================================================= sonarr.ginnoir.com { import internal_only route { import authentik_outpost handle /api/* { reverse_proxy sonarr:8989 } handle { import authentik_forward_auth reverse_proxy sonarr:8989 } } } radarr.ginnoir.com { import internal_only route { import authentik_outpost handle /api/* { reverse_proxy radarr:7878 } handle { import authentik_forward_auth reverse_proxy radarr:7878 } } } bazarr.ginnoir.com { import internal_only route { import authentik_outpost handle /api/* { reverse_proxy bazarr:6767 } handle { import authentik_forward_auth reverse_proxy bazarr:6767 } } } prowlarr.ginnoir.com { import internal_only route { import authentik_outpost handle /api/* { reverse_proxy prowlarr:9696 } handle { import authentik_forward_auth reverse_proxy prowlarr:9696 } } } tautulli.ginnoir.com { import internal_only route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy tautulli:8181 } } } # ============================================================= # DOWNLOAD CLIENTS — internal only # ============================================================= qbittorrent.ginnoir.com { import internal_only route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy qbittorrent:3232 } } } nzbget.ginnoir.com { import internal_only route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy nzbget:6789 } } } whisparr.ginnoir.com { import internal_only route { import authentik_outpost handle /api/* { reverse_proxy whisparr:6969 } handle { import authentik_forward_auth reverse_proxy whisparr:6969 } } } stash.ginnoir.com { import internal_only route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy stash:6970 } } } # ============================================================= # GAMES / ROMS — internal only # External access for a trusted friend is via the `share` stack's Tailscale # serve node (roms-share..ts.net), NOT public exposure here. # ============================================================= roms.ginnoir.com { import share_gate reverse_proxy romm:8080 } romhacks.ginnoir.com { import share_gate root * /srv/romhacks-wiki file_server } jd.ginnoir.com { import internal_only reverse_proxy jdownloader:5800 } idlegame.ginnoir.com { import internal_only root * /srv/idlegame try_files {path} /index.html file_server } # 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 # stack; notes embed art via https://romhacks-files.ginnoir.com//. romhacks-files.ginnoir.com { import internal_only handle_path /_roms/* { root * /srv/roms file_server } handle { root * /srv/romhacks file_server browse } } # ============================================================= # NOTIFICATIONS & RSS — public (ntfy native auth; FreshRSS HTTP auth via Authentik) # ============================================================= ntfy.ginnoir.com, http://ntfy.ginnoir.com { reverse_proxy ntfy:80 @httpget { protocol http method GET path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) } redir @httpget https://{host}{uri} } freshrss.ginnoir.com { reverse_proxy freshrss:80 } # ============================================================= # RESUME / PORTFOLIO — public (native OIDC via Authentik; no forward_auth) # ============================================================= resume.ginnoir.com { reverse_proxy app:3000 } j-costa.com, https://j-costa.com { tls { issuer acme { disable_tlsalpn_challenge } } redir * https://resume.ginnoir.com/ginnoir/resume permanent } storage.j-costa.com, https://storage.j-costa.com { tls { issuer acme { disable_tlsalpn_challenge } } reverse_proxy resume-minio:9000 } # ============================================================= # MINIO CONSOLE — internal only # ============================================================= minio.ginnoir.com { import internal_only reverse_proxy resume-minio:9001 } # ============================================================= # FAMAPP & AUTH — public # ============================================================= fam.ginnoir.com { reverse_proxy famapp:3000 } auth.ginnoir.com { reverse_proxy authentik-server:9000 } dev.ginnoir.com { reverse_proxy 192.168.1.74:3000 } # ============================================================= # MANAGEMENT — internal only # ============================================================= portainer.ginnoir.com { @webhook { method POST path /api/stacks/webhooks/* } handle @webhook { reverse_proxy portainer:9000 } handle { reverse_proxy portainer:9000 } } uptime.ginnoir.com { route { import authentik_outpost @uptime_mcp { path /api/* /socket.io/* import remote_internal } handle @uptime_mcp { reverse_proxy uptime-kuma:3001 } handle { import authentik_forward_auth reverse_proxy uptime-kuma:3001 } } } homarr.ginnoir.com { import internal_only reverse_proxy homarr:7575 } # Hermes Desktop remote backend. Only API/WS is exposed for native # clients; the browser dashboard is blocked so its injected session token # is not leaked to the public web. hermes.ginnoir.com { route { handle /api/* { reverse_proxy 172.20.0.1:9119 { header_up Host 172.20.0.1:9119 } } handle { respond "Hermes dashboard browser UI is disabled on this public hostname." 403 } } } # Hermes WebUI for browser/phone access to the valhalla Hermes runtime. # Internal-only; WebUI also enforces its own password auth. webui.ginnoir.com { import internal_only reverse_proxy 172.20.0.1:8787 } # ComfyUI node editor — inference runs on the Mac at 192.168.1.121 (Metal/MPS). # LAN/tailnet only; no Authentik (WebSocket queue/progress breaks under forward_auth). comfyui.ginnoir.com { import internal_only reverse_proxy 192.168.1.121:8188 } backup.ginnoir.com { route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy kopia:51515 } } } router.ginnoir.com { import internal_only reverse_proxy 192.168.1.1 } # ============================================================= # DEV STACK — internal only # ============================================================= gitea.ginnoir.com { @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 } } code.ginnoir.com { route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy code_server:8443 } } } registry.ginnoir.com { import internal_only reverse_proxy registry:5000 } registry-ui.ginnoir.com { route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy registry_ui:80 } } } dbx.ginnoir.com { route { import authentik_outpost handle { import authentik_forward_auth reverse_proxy dbx:4224 } } } vault.ginnoir.com { route { import authentik_outpost @vaultapi path /v1/* handle @vaultapi { reverse_proxy vault:8200 } handle { import authentik_forward_auth reverse_proxy vault:8200 } } } docs.ginnoir.com { import internal_only reverse_proxy bookstack:80 } plane.ginnoir.com { route { import authentik_outpost @plane_api { path /api/* header X-API-Key * } handle @plane_api { reverse_proxy plane_api:8000 } handle /api/* { import authentik_forward_auth reverse_proxy plane_api:8000 } handle /auth/* { import authentik_forward_auth reverse_proxy plane_api:8000 } handle /god-mode/* { import authentik_forward_auth reverse_proxy plane_admin:3000 } handle { import authentik_forward_auth reverse_proxy plane_web:3000 } } } # ============================================================= # FILES / DOCUMENTS — internal only # files.ginnoir.com is the primary domain; nextcloud.ginnoir.com is an alias. # office.ginnoir.com serves the OnlyOffice document server. # ============================================================= nextcloud.ginnoir.com { import internal_only redir /.well-known/carddav /remote.php/dav 301 redir /.well-known/caldav /remote.php/dav 301 reverse_proxy nextcloud:80 { header_up X-Forwarded-Proto https header_up X-Real-IP {remote_host} } } office.ginnoir.com { import internal_only reverse_proxy onlyoffice-docs:80 } # ============================================================= # PHOTOS — internal only # ============================================================= photos.ginnoir.com { import internal_only reverse_proxy immich-server:2283 } # ============================================================= # NOTES / PKM — internal only (LiveSync over LAN or tailnet) # ============================================================= obsidian.ginnoir.com { import internal_only reverse_proxy couchdb:5984 }