diff --git a/.gitea/workflows/tangle.yaml b/.gitea/workflows/tangle.yaml new file mode 100644 index 0000000..2613ec0 --- /dev/null +++ b/.gitea/workflows/tangle.yaml @@ -0,0 +1,23 @@ +name: Tangle and Deploy +on: [push] + +jobs: + tangle: + runs-on: debian-latest + steps: + - uses: actions/checkout@v4 + + - name: Tangle infrastructure.org + run: | + docker run --rm \ + -v /:/host \ + -v $(pwd):/workspace:ro \ + debian:stable-slim \ + bash -c "cp -r /workspace /host/tmp/infra-tangle && chroot /host /usr/local/bin/tangle-deploy /tmp/infra-tangle" + + - name: Restart affected services + run: | + docker run --rm \ + -v /:/host \ + debian:stable-slim \ + bash -c "chroot /host bash -c 'cd /docker/compose && docker compose up -d traefik 2>&1'" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dd033a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.yaml.bak +*.yaml.bak2 +*~ diff --git a/infrastructure.org b/infrastructure.org index f37e9b3..69b2a0a 100644 --- a/infrastructure.org +++ b/infrastructure.org @@ -41,6 +41,965 @@ Service-to-service / automation / cross-VLAN 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) @@ -99,43 +1058,6 @@ www.gharbeia.net: - SSO button added to login page via Jellyfin branding config - No Forward Auth — Jellyfin handles auth itself via plugin -* Traefik Configuration - -** Source of Truth -- Compose labels: =/docker/compose/docker-compose.yaml= and companion files -- Internal routers (auth): =/docker/compose/traefik-internal.yaml= -- Internal routers (noauth): =/docker/compose/traefik-internal-noauth.yaml= -- Runtime copy: =/docker/appdata/traefik/internal.yaml= and =internal-noauth.yaml= -- Deploy: =restart.sh= copies all yaml files to =/docker/appdata/traefik/= then =docker compose up -d traefik= - -** Entrypoints -- =tunnel= (port 8081) :: Cloudflare Tunnel traffic (external) -- =secureweb= (port 443) :: Internal LAN traffic, TLS, with Authentik Forward Auth -- =internal= (port 8083) :: Internal service-to-service, NO Authentik auth, HTTP only - -** External Routers (tunnel entrypoint) -- Defined in compose labels -- All behind Authentik Forward Auth middleware (except Jellyfin, Gitea) - -** Internal Routers — Authenticated - -Defined in =traefik-internal.yaml= (=internal.yaml= at runtime) -All on entrypoint =secureweb= (port 443, TLS) -All behind Authentik Forward Auth middleware -Common middleware chain: =auth@file, security-headers@file, traefik-bouncer@file= - -** Internal Routers — Authless - -Defined in =traefik-internal-noauth.yaml= (=internal-noauth.yaml= at runtime) -All on entrypoint =internal= (port 8083, HTTP) -No Authentik Forward Auth (no =authentik-forwardauth@file= middleware) -Same services as the authenticated routers, same backend URLs -Common middleware chain: =security-headers@file, traefik-bouncer@file= -Used for: runners, cross-VLAN tooling, service-to-service API calls - -Services already authless on secureweb (Gitea, gharbeia-site) also have -noauth copies for consistency. - * LOGBOOK ** [2026-05-15 Thu 06:10] Static site launched @@ -145,24 +1067,25 @@ noauth copies for consistency. - 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 -- Copied to runtime: `/docker/appdata/traefik/internal-noauth.yaml` -- Exposed port 8083 on traefik container (`INTERNAL_PORT_TRAEFIK=8083`) -- Added to restart.sh deployment: `sudo cp traefik-internal-noauth.yaml $FOLDER_FOR_DATA/traefik/internal-noauth.yaml` -- Unbound already resolves `*.gharbeia.net` → `10.10.10.201` via `local-zone redirect` — no changes needed -- Docker containers already inherit this DNS through embedded DNS (127.0.0.11) → host DNS → Unbound -- Updated Gitea runner: `GITEA_INSTANCE_URL: http://10.10.10.201:3001` → `http://git.gharbeia.net:8083` -- Verified: runner registers and communicates through domain-based URL -- Gitea config already used domains: `ROOT_URL = https://git.gharbeia.net/` -- Gluetun extra_hosts kept as-is (safety net for VPN namespace DNS leaks) -- Architecture: browsers use =:443= (secureweb, with auth); services/automation use =:8083= (internal, no auth) +- 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) -- Config.yml on disk had correct entries but ISN'T used at runtime (tunnel runs with =--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 diff --git a/tangle-deploy.sh b/tangle-deploy.sh new file mode 100644 index 0000000..4ce2441 --- /dev/null +++ b/tangle-deploy.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# tangle-deploy — Tangle infrastructure.org and restart affected services +# Called by Gitea Action runner after git push, or directly from CLI. +# +# Usage: +# tangle-deploy # uses /docker/compose/infrastructure +# tangle-deploy /path/to/repo # uses provided path (e.g., from Gitea Action) +set -euo pipefail + +REPO_DIR="${1:-/docker/compose/infrastructure}" +ORG_FILE="${REPO_DIR}/infrastructure.org" + +# If called with a workspace path from Gitea Action, use it as-is. +# Otherwise, ensure we have the latest from git. +if [ -z "${1:-}" ]; then + if [ ! -d "$REPO_DIR" ]; then + git clone ssh://git@10.10.10.201:2222/amr/infrastructure.git "$REPO_DIR" + else + cd "$REPO_DIR" && git pull + fi +fi + +if [ ! -f "$ORG_FILE" ]; then + echo "ERROR: $ORG_FILE not found in $REPO_DIR" + exit 1 +fi + +echo "=== Tangling $ORG_FILE ===" +emacs --batch -Q --load /usr/share/emacs/site-lisp/org/org-loaddefs.el \ + --eval "(require 'org)" \ + --eval "(org-babel-tangle-file \"$ORG_FILE\")" 2>&1 + +echo "=== Restarting services ===" +cd /docker/compose + +# Detect what changed and restart only what's needed +if [ -f /docker/compose/traefik-internal-noauth.yaml ] || \ + [ -f /docker/compose/traefik-static.yaml ] || \ + [ -f /docker/compose/traefik-internal.yaml ] || \ + [ -f /docker/compose/traefik-dynamic.yaml ]; then + echo "Traefik config changed — restarting..." + docker compose up -d traefik +fi + +if [ -f /docker/compose/unbound/unbound.conf ]; then + echo "Unbound config changed — restarting..." + docker compose up -d unbound +fi + +if [ -f /docker/compose/docker-compose.yaml ]; then + echo "Docker compose changed — restarting all services" + docker compose up -d 2>&1 | tail -5 +fi + +echo "=== Deploy complete ==="