Some checks failed
Tangle and Deploy / tangle (push) Failing after 12s
- 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
1092 lines
32 KiB
Org Mode
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
|