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

32 KiB

Infrastructure Documentation — gharbeia.net

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.

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

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.

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

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.

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

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.

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

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:

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.net10.10.10.201 via local-zone redirect — no changes needed
  • Updated Gitea runner: GITEA_INSTANCE_URLhttp://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