Files
infrastructure/infrastructure.org
Hermes 2a01bed005
Some checks failed
Tangle and Deploy / tangle (push) Failing after 12s
feat: literate IaC with tangle-deploy pipeline
- Converted Traefik section to tangle blocks with absolute paths
- Created .gitea/workflows/tangle.yaml Gitea Action
- tangle-deploy.sh: tangles org → writes files → restarts services
2026-05-15 07:12:24 +00:00

1092 lines
32 KiB
Org Mode

#+TITLE: Infrastructure Documentation — gharbeia.net
#+AUTHOR: Amr Gharbeia
#+DATE: 2026-05-15
* Architecture
** Hosts
- =production-1= (10.10.10.201) :: Docker host, runs all services
- Hermes Agent :: Management/automation host
** Network
- Docker network =networking= (172.28.10.0/24)
- Proxmox VLANs: 1/10/20/30/40/50
- Services VLAN: 10.10.10.0/24
- Domain: gharbeia.net via Cloudflare (orange cloud/proxied)
** External Access Architecture
Cloudflare (edge, orange cloud)
└─ Cloudflare Tunnel "home" (cloudflared on production-1)
└─ Traefik (entrypoint=tunnel, port 8081)
├─ Authentik Forward Auth (external routers)
├─ gharbeia-site (nginx)
├─ jellyfin (SSO via plugin + OIDC)
├─ gitea (native OIDC)
└─ *.gharbeia.net services
** Internal Access Architecture
LAN client (browser)
└─ Traefik (entrypoint=secureweb, port 443)
├─ Authentik Forward Auth (internal.yaml routers)
├─ gharbeia-site (public, no auth)
├─ jellyfin (SSO via plugin)
└─ *.gharbeia.net services
Service-to-service / automation / cross-VLAN
└─ Traefik (entrypoint=internal, port 8083 — NO auth)
└─ Same routing as secureweb, from =traefik-internal-noauth.yaml=
Key distinction: =:443= = browsers/humans with Authentik auth.
=:8083= = runners, automated tooling, services on other VLANs.
* Traefik — Reverse Proxy
Traefik is the edge router for all HTTP traffic. It handles TLS termination via
Let's Encrypt (DNS-01 challenge through Cloudflare), routes traffic to the right
container, and applies middleware chains for auth, security, and rate limiting.
Three entrypoints:
- =tunnel= (=:8081=) :: Receives traffic from the Cloudflare tunnel. All routers
here have Authentik Forward Auth.
- =secureweb= (=:443=) :: Internal LAN traffic with TLS. Also has Authentik
Forward Auth for browser access.
- =internal= (=:8083=) :: Service-to-service and cross-VLAN traffic. No auth.
HTTP only. For runners, automation, and API calls that shouldn't hit Authentik.
** Static Configuration
The static config sets entrypoints, TLS resolvers, providers, and plugins.
It is the foundation everything else builds on.
#+BEGIN_SRC yaml :tangle /docker/compose/traefik-static.yaml
global:
checkNewVersion: true
sendAnonymousUsage: true
log:
level: INFO
accessLog:
filePath: /var/log/access.log
format: json
api:
dashboard: true
insecure: true
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: secureweb
scheme: https
permanent: true
tunnel:
address: :8081
secureweb:
address: :443
http:
tls:
options: default
certResolver: letsencrypt
domains:
- main: gharbeia.net
sans:
- "*.gharbeia.net"
internal:
address: :8083
metrics:
address: :8082
metrics:
prometheus:
entryPoint: metrics
manualRouting: true
headerLabels:
useragent: User-Agent
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
providers:
docker:
exposedByDefault: false
file:
directory: /etc/traefik
watch: true
certificatesResolvers:
letsencrypt:
acme:
storage: /letsencrypt/acme.json
email: gharbeia@riseup.net
keyType: EC384
caServer: https://acme-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: cloudflare
resolvers:
- 1.1.1.1:53
- 1.0.0.1:53
propagation:
delayBeforeChecks: 60s
experimental:
plugins:
crowdsec-bouncer-traefik-plugin:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.2
#+END_SRC
Why each piece:
- =web= (=:80=) exists only to redirect to HTTPS. No TLS.
- =tunnel= (=:8081=) is inbound-only from cloudflared, never exposed to LAN.
Cloudflare handles TLS at the edge, so this can be plain HTTP inside Docker.
- =secureweb= (=:443=) is the LAN-facing entrypoint with Let's Encrypt certs
covering both =gharbeia.net= and =*.gharbeia.net=.
- =internal= (=:8083=) is plain HTTP for service-to-service traffic. TLS overhead
is unnecessary on the internal bridge network.
- =metrics= (=:8082=) exposes Prometheus metrics, manually routed.
- =dnsChallenge= with Cloudflare provider issues wildcard certs. The 60s
propagation delay avoids rate-limit issues with Cloudflare's API.
** Dynamic Configuration — Middleware
Shared middleware used by all routers. Defined once here, referenced by name
in every router block.
#+BEGIN_SRC yaml :tangle /docker/compose/traefik-dynamic.yaml
http:
middlewares:
authentik-forwardauth:
forwardAuth:
address: http://authentik-server:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
security-headers:
headers:
customFrameOptionsValue: SAMEORIGIN
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: no-referrer
permissionsPolicy: ""
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow"
Server: ""
traefik-bouncer:
plugin:
crowdsec-bouncer-traefik-plugin:
enabled: "true"
crowdsecMode: live
crowdsecLapiKey: __CROWDSEC_LAPI_KEY__
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
updateFrequencySec: 5
defaultDecisionLifetimeSec: 60
compress:
compress:
excludedContentTypes:
- text/event-stream
ratelimit:
rateLimit:
average: 100
burst: 50
#+END_SRC
The auth flow: Authentik's outpost runs as a sidecar (/authentik-server/ in
Docker) that validates session cookies. When a request lacks a valid session,
Traefik redirects to the Authentik login page. After login, Authentik redirects
back to the original URL with a session cookie.
=security-headers= locks down XSS, clickjacking, and fingerprinting. The empty
=permissionsPolicy= disables all browser APIs by default.
=traefik-bouncer= runs CrowdSec's LAPI bouncer as a Traefik plugin. IPs flagged
by CrowdSec get blocked. The LAPI key is a placeholder — fill from vault.
** Internal Routers — Authenticated (secureweb :443)
These routers serve LAN browser traffic. All have Authentik Forward Auth.
Backend services are referenced by Docker DNS name on the =networking= bridge.
#+BEGIN_SRC yaml :tangle /docker/compose/traefik-internal.yaml
http:
routers:
# ── Media & Streaming ─────────────────────────────────────────
jellyfin:
rule: "Host(`jellyfin.gharbeia.net`)"
service: jellyfin-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
jellyseerr:
rule: "Host(`jellyseerr.gharbeia.net`)"
service: jellyseerr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── *arr Suite ────────────────────────────────────────────────
radarr:
rule: "Host(`radarr.gharbeia.net`)"
service: radarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
sonarr:
rule: "Host(`sonarr.gharbeia.net`)"
service: sonarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
lidarr:
rule: "Host(`lidarr.gharbeia.net`)"
service: lidarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
prowlarr:
rule: "Host(`prowlarr.gharbeia.net`)"
service: prowlarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
whisparr:
rule: "Host(`whisparr.gharbeia.net`)"
service: whisparr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
mylar:
rule: "Host(`mylar.gharbeia.net`)"
service: mylar-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
lazylibrarian:
rule: "Host(`lazylibrarian.gharbeia.net`)"
service: lazylibrarian-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Downloaders ───────────────────────────────────────────────
sabnzbd:
rule: "Host(`sabnzbd.gharbeia.net`)"
service: sabnzbd-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
qbittorrent:
rule: "Host(`qbittorrent.gharbeia.net`)"
service: qbittorrent-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
flaresolverr:
rule: "Host(`flaresolverr.gharbeia.net`)"
service: flaresolverr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Homepage / Dashboards ─────────────────────────────────────
homepage:
rule: "Host(`homepage.gharbeia.net`)"
service: homepage-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
homarr:
rule: "Host(`homarr.gharbeia.net`)"
service: homarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
heimdall:
rule: "Host(`heimdall.gharbeia.net`)"
service: heimdall-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Monitoring ────────────────────────────────────────────────
grafana:
rule: "Host(`grafana.gharbeia.net`)"
service: grafana-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
prometheus:
rule: "Host(`prometheus.gharbeia.net`)"
service: prometheus-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Website (public, no auth) ─────────────────────────────────
gharbeia-site:
rule: "Host(`gharbeia.net`) || Host(`www.gharbeia.net`)"
service: gharbeia-site-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
# ── Management ────────────────────────────────────────────────
gitea:
rule: "Host(`git.gharbeia.net`)"
service: gitea-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- security-headers@file
- traefik-bouncer@file
# No authentik-forwardauth — Gitea has native OIDC
portainer:
rule: "Host(`portainer.gharbeia.net`)"
service: portainer-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
guacamole:
rule: "Host(`guacamole.gharbeia.net`)"
service: guacamole-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
headplane:
rule: "Host(`headplane.gharbeia.net`) && PathPrefix(`/admin/`)"
service: headplane-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
traefik-dashboard:
rule: "Host(`traefik.gharbeia.net`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
service: traefik-dashboard-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Other ─────────────────────────────────────────────────────
bazarr:
rule: "Host(`bazarr.gharbeia.net`)"
service: bazarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
tdarr:
rule: "Host(`tdarr.gharbeia.net`)"
service: tdarr-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
ddns-updater:
rule: "Host(`ddns-updater.gharbeia.net`)"
service: ddns-updater-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
stash:
rule: "Host(`stash.gharbeia.net`)"
service: stash-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
audiobookshelf:
rule: "Host(`audiobookshelf.gharbeia.net`)"
service: audiobookshelf-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── External hardware (via LAN) ───────────────────────────────
synology:
rule: "Host(`synology.gharbeia.net`)"
service: synology-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
gateway:
rule: "Host(`gateway.gharbeia.net`)"
service: gateway-internal
entryPoints:
- secureweb
tls:
certResolver: letsencrypt
middlewares:
- authentik-forwardauth@file
- security-headers@file
- traefik-bouncer@file
# ── Services (Docker DNS backends) ──────────────────────────────
services:
# VPN-routed (through Gluetun network namespace)
jellyfin-internal:
loadBalancer:
servers:
- url: "http://gluetun:8096"
jellyseerr-internal:
loadBalancer:
servers:
- url: "http://gluetun:5055"
radarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:7878"
sonarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:8989"
lidarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:8686"
prowlarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:9696"
whisparr-internal:
loadBalancer:
servers:
- url: "http://gluetun:6969"
mylar-internal:
loadBalancer:
servers:
- url: "http://gluetun:8090"
lazylibrarian-internal:
loadBalancer:
servers:
- url: "http://gluetun:5299"
sabnzbd-internal:
loadBalancer:
servers:
- url: "http://gluetun:8080"
qbittorrent-internal:
loadBalancer:
servers:
- url: "http://gluetun:8200"
flaresolverr-internal:
loadBalancer:
servers:
- url: "http://gluetun:8191"
bazarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:6767"
tdarr-internal:
loadBalancer:
servers:
- url: "http://gluetun:8265"
stash-internal:
loadBalancer:
servers:
- url: "http://gluetun:7777"
audiobookshelf-internal:
loadBalancer:
servers:
- url: "http://gluetun:13378"
# Direct Docker DNS
homepage-internal:
loadBalancer:
servers:
- url: "http://homepage:3000"
homarr-internal:
loadBalancer:
servers:
- url: "http://homarr:7575"
heimdall-internal:
loadBalancer:
servers:
- url: "http://heimdall:80"
grafana-internal:
loadBalancer:
servers:
- url: "http://grafana:3000"
prometheus-internal:
loadBalancer:
servers:
- url: "http://prometheus:9090"
gitea-internal:
loadBalancer:
servers:
- url: "http://gitea:3000"
portainer-internal:
loadBalancer:
servers:
- url: "http://portainer:9000"
guacamole-internal:
loadBalancer:
servers:
- url: "http://guacamole:8080"
headplane-internal:
loadBalancer:
servers:
- url: "http://headplane:3000"
traefik-dashboard-internal:
loadBalancer:
servers:
- url: "http://traefik:8080"
ddns-updater-internal:
loadBalancer:
servers:
- url: "http://ddns-updater:8310"
gharbeia-site-internal:
loadBalancer:
servers:
- url: "http://gharbeia-site:80"
# External hardware
synology-internal:
loadBalancer:
servers:
- url: "https://192.168.1.8:5001"
passHostHeader: true
serversTransport: insecure-no-verify
gateway-internal:
loadBalancer:
servers:
- url: "https://192.168.1.1"
passHostHeader: true
serversTransport: insecure-no-verify
serversTransports:
insecure-no-verify:
insecureSkipVerify: true
#+END_SRC
Services behind Gluetun use =http://gluetun:PORT= because those containers share
Gluetun's network namespace via =network_mode: service:gluetun=. Traefik reaches
them via Docker DNS through the =networking= bridge.
External hardware (Synology, gateway) use =serversTransport: insecure-no-verify=
because their self-signed certs would otherwise fail Traefik's TLS verification.
** Internal Routers — Authless (internal :8083)
Identical routing to the authenticated routers, but on entrypoint =internal=
(port 8083) and without =authentik-forwardauth@file= middleware. Used by
automation, runners, and cross-VLAN tooling.
#+BEGIN_SRC yaml :tangle /docker/compose/traefik-internal-noauth.yaml
http:
routers:
jellyfin-noauth:
rule: "Host(`jellyfin.gharbeia.net`)"
service: jellyfin-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
jellyseerr-noauth:
rule: "Host(`jellyseerr.gharbeia.net`)"
service: jellyseerr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
radarr-noauth:
rule: "Host(`radarr.gharbeia.net`)"
service: radarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
sonarr-noauth:
rule: "Host(`sonarr.gharbeia.net`)"
service: sonarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
lidarr-noauth:
rule: "Host(`lidarr.gharbeia.net`)"
service: lidarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
prowlarr-noauth:
rule: "Host(`prowlarr.gharbeia.net`)"
service: prowlarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
whisparr-noauth:
rule: "Host(`whisparr.gharbeia.net`)"
service: whisparr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
mylar-noauth:
rule: "Host(`mylar.gharbeia.net`)"
service: mylar-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
lazylibrarian-noauth:
rule: "Host(`lazylibrarian.gharbeia.net`)"
service: lazylibrarian-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
sabnzbd-noauth:
rule: "Host(`sabnzbd.gharbeia.net`)"
service: sabnzbd-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
qbittorrent-noauth:
rule: "Host(`qbittorrent.gharbeia.net`)"
service: qbittorrent-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
flaresolverr-noauth:
rule: "Host(`flaresolverr.gharbeia.net`)"
service: flaresolverr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
homepage-noauth:
rule: "Host(`homepage.gharbeia.net`)"
service: homepage-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
homarr-noauth:
rule: "Host(`homarr.gharbeia.net`)"
service: homarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
heimdall-noauth:
rule: "Host(`heimdall.gharbeia.net`)"
service: heimdall-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
grafana-noauth:
rule: "Host(`grafana.gharbeia.net`)"
service: grafana-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
prometheus-noauth:
rule: "Host(`prometheus.gharbeia.net`)"
service: prometheus-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
gharbeia-site-noauth:
rule: "Host(`gharbeia.net`) || Host(`www.gharbeia.net`)"
service: gharbeia-site-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
gitea-noauth:
rule: "Host(`git.gharbeia.net`)"
service: gitea-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
portainer-noauth:
rule: "Host(`portainer.gharbeia.net`)"
service: portainer-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
guacamole-noauth:
rule: "Host(`guacamole.gharbeia.net`)"
service: guacamole-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
headplane-noauth:
rule: "Host(`headplane.gharbeia.net`) && PathPrefix(`/admin/`)"
service: headplane-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
traefik-dashboard-noauth:
rule: "Host(`traefik.gharbeia.net`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
service: traefik-dashboard-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
bazarr-noauth:
rule: "Host(`bazarr.gharbeia.net`)"
service: bazarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
tdarr-noauth:
rule: "Host(`tdarr.gharbeia.net`)"
service: tdarr-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
ddns-updater-noauth:
rule: "Host(`ddns-updater.gharbeia.net`)"
service: ddns-updater-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
stash-noauth:
rule: "Host(`stash.gharbeia.net`)"
service: stash-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
audiobookshelf-noauth:
rule: "Host(`audiobookshelf.gharbeia.net`)"
service: audiobookshelf-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
synology-noauth:
rule: "Host(`synology.gharbeia.net`)"
service: synology-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
gateway-noauth:
rule: "Host(`gateway.gharbeia.net`)"
service: gateway-internal
entryPoints:
- internal
middlewares:
- security-headers@file
- traefik-bouncer@file
#+END_SRC
* Services
** gharbeia-site (Static Website)
- Container: =gharbeia-site= (nginx:stable-alpine3.17-perl)
- Purpose: Landing page for gharbeia.net
- Docroot: =/docker/appdata/gharbeia-site/html=
- Nginx config: =/docker/appdata/gharbeia-site/nginx.conf=
- Traefik router: =gharbeia-site= on entrypoints =tunnel= and =secureweb=
~www.gharbeia.net~ → 301 redirect → ~gharbeia.net~ (handled by nginx)
Both domains in Traefik router rule: ~Host(\`gharbeia.net\`) || Host(\`www.gharbeia.net\`)~
** Cloudflare Tunnel "home"
- Container: =cloudflared= (cloudflare/cloudflared:latest)
- Config: =/docker/compose/cloudflared-config.yml= (local, unused at runtime)
- Runtime: =docker compose up -d cloudflared= with =--token= (remote config from dashboard)
- Local config is IGNORED when running with =--token= — ingress rules come from Cloudflare Zero Trust dashboard's public hostname configuration
- DNS CNAME records must point to =<tunnel-uuid>.cfargotunnel.com=
- Tunnel UUID: =c29295c5-946a-4ddf-bdfe-7eafcd74faa3=
*** Public Hostnames (Cloudflare Dashboard)
These must be added in Cloudflare Zero Trust > Networks > Tunnels > home > Public Hostnames:
- *.gharbeia.net → https://traefik:443
- gharbeia.net → https://traefik:443 (must be explicit, wildcard doesn't cover root)
- www.gharbeia.net → https://traefik:443
*** DNS Records
gharbeia.net:
- CNAME → c29295c5-946a-4ddf-bdfe-7eafcd74faa3.cfargotunnel.com (proxied)
- MX → in1-smtp.messagingengine.com
- MX → in2-smtp.messagingengine.com
- TXT → v=spf1 include:spf.messagingengine.com ?all
www.gharbeia.net:
- CNAME → c29295c5-946a-4ddf-bdfe-7eafcd74faa3.cfargotunnel.com (proxied)
* Authentication
** Authentik (IdP)
- Provides SSO for all services
- Two modes: Forward Auth (proxy-level) and OIDC (service-level)
- External tunnel traffic: Forward Auth on all routers in compose labels
- Internal LAN: Forward Auth on all routers in internal.yaml
- Exceptions: Jellyfin (SSO plugin), Gitea (native OIDC)
** Gitea — Native OIDC
- Configured in Gitea → Site Administration → Authentication Sources
- Authentik OIDC provider registered
- Works with native Gitea clients (no browser redirect needed)
** Jellyfin — SSO-Auth Plugin v4.0.0.4
- Plugin: SSO-Auth (via Jellyfin plugin catalog)
- Authentik OIDC provider created, redirect URI: ~https://jellyfin.gharbeia.net/sso/OID/redirect/Authentik~
- Scope mapping sends ~groups~ claim in OpenID token
- Plugin configured via API (=docker cp= XML into container)
- SSO button added to login page via Jellyfin branding config
- No Forward Auth — Jellyfin handles auth itself via plugin
* LOGBOOK
** [2026-05-15 Thu 06:10] Static site launched
- Setup gharbeia.net and www.gharbeia.net with nginx container
- Tunnel + Traefik wiring
- www → root 301 redirect in nginx config
- Traefik router on both tunnel and secureweb entrypoints
** [2026-05-15 Thu 06:38] Internal authless entrypoint + domain migration
- Added Traefik =internal= entrypoint (port 8083) for authless service-to-service traffic
- Created =/docker/compose/traefik-internal-noauth.yaml= with 28 router copies
- Exposed port 8083 (=INTERNAL_PORT_TRAEFIK=8083=)
- Unbound already resolves =*.gharbeia.net==10.10.10.201= via =local-zone redirect= — no changes needed
- Updated Gitea runner: =GITEA_INSTANCE_URL==http://git.gharbeia.net:8083=
- Architecture settled: =:443= = browsers with auth, =:8083= = services without
** [2026-05-15 Thu 06:18] Error 1033 on gharbeia.net
- Problem: CNAME for ~gharbeia.net~ pointed to old tunnel (2cd53dc4-...), not "home" tunnel (c29295c5-...)
- www.gharbeia.net worked because its CNAME was correct
- www → root redirect → Cloudflare tried old tunnel → 1033
- Fix: Updated CNAME DNS record via Cloudflare API (DNS token)
- Lesson: Bare domain DNS must point to the same tunnel UUID as subdomains
- Lesson: The local cloudflared-config.yml is decorative when running with =--token=
** [2026-05-15 Thu 03:07] Literate infrastructure established
- infrastructure.org becomes the source of truth — all config files are
tangle targets embedded as =#+BEGIN_SRC= blocks with absolute paths
- =tangle-deploy= script installed at =/usr/local/bin/tangle-deploy= on
production-1; run after git push to regenerate configs and restart services
- Gitea repo: =git@git.gharbeia.net:amr/infrastructure.git=
- Emacs-nox installed on production-1 for headless tangle