From 5d6a752f715a485f1988ea66a3250ee2242ea50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Aug 2026 18:56:53 +0200 Subject: [PATCH] expose Traefik dashboard on traefik.pivoine.art, vpn-only gated Previous attempt tried a separate internal-only :8080 entrypoint for pulsenode to reach the API directly - that turned out to 404 no matter what (--api.dashboard=true alone didn't register a working router there, and adding --api=true plus an explicit entrypoint still didn't produce a matching router; verified against a local traefik:v3 container before giving up on that path rather than keep guessing against the live one). Cleaner approach: expose it through the same web/web-secure entrypoints everything else already uses, via a new file-provider router (traefik/dynamic/dashboard.yaml) matching Traefik's own documented self-referencing pattern - Host(`traefik.pivoine.art`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`)) routed to the built-in api@internal service, gated by vpn-only same as pulsenode's own router. vpn-only's sourceRange now also includes falcon_network's own subnet (172.18.0.0/16) alongside the tailnet ranges - a container on our own internal docker network is as trusted as a tailnet peer, and without this pulsenode's own request to the API (a container-to-container call, not a VPN-sourced one) would get the same 403 a random internet visitor would. pulsenode's traefik widget now points at https://traefik.pivoine.art/api instead of the internal :8080 attempt. --- pulsenode/config/config.yml | 2 +- traefik/compose.yml | 4 +--- traefik/dynamic/dashboard.yaml | 32 ++++++++++++++++++++++++++++++++ traefik/dynamic/security.yaml | 10 +++++++--- 4 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 traefik/dynamic/dashboard.yaml diff --git a/pulsenode/config/config.yml b/pulsenode/config/config.yml index 0a64a80..faeb163 100644 --- a/pulsenode/config/config.yml +++ b/pulsenode/config/config.yml @@ -12,7 +12,7 @@ groups: - type: traefik name: Traefik - apiUrl: http://traefik:8080/api + apiUrl: https://traefik.pivoine.art/api interval: 15s - type: docker diff --git a/traefik/compose.yml b/traefik/compose.yml index 565bf6a..4ad76ca 100644 --- a/traefik/compose.yml +++ b/traefik/compose.yml @@ -3,6 +3,7 @@ services: image: traefik:v3 container_name: traefik command: + - "--api=true" - "--api.dashboard=true" - "--ping=true" - "--log.level=INFO" @@ -16,9 +17,6 @@ services: - "--providers.file.watch=true" - "--entrypoints.web.address=:80" - "--entrypoints.web-secure.address=:443" - # Internal-only: not in ports: below, so only reachable from other - # containers on falcon_network (i.e. pulsenode's traefik widget). - - "--entrypoints.traefik.address=:8080" - "--entrypoints.web.http.redirections.entryPoint.to=web-secure" - "--entrypoints.web.http.redirections.entryPoint.scheme=https" - "--entrypoints.web.http.redirections.entryPoint.permanent=true" diff --git a/traefik/dynamic/dashboard.yaml b/traefik/dynamic/dashboard.yaml new file mode 100644 index 0000000..383711f --- /dev/null +++ b/traefik/dynamic/dashboard.yaml @@ -0,0 +1,32 @@ +# Exposes Traefik's own API/dashboard (api@internal is a built-in service, +# not something defined below) on traefik.pivoine.art, gated by vpn-only. +# Path prefixes match Traefik's own documented self-referencing pattern: +# /api serves the JSON pulsenode's traefik widget reads, /dashboard serves +# the browsable UI. +http: + middlewares: + traefik-redirect-web-secure: + redirectScheme: + scheme: https + + routers: + traefik-dashboard-web: + rule: "Host(`traefik.pivoine.art`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + entrypoints: + - web + middlewares: + - vpn-only@file + - traefik-redirect-web-secure + service: api@internal + + traefik-dashboard-web-secure: + rule: "Host(`traefik.pivoine.art`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + entrypoints: + - web-secure + tls: + certResolver: resolver + middlewares: + - vpn-only@file + - security-headers@file + - no-index@file + service: api@internal diff --git a/traefik/dynamic/security.yaml b/traefik/dynamic/security.yaml index 8b95bf5..dbf65bf 100644 --- a/traefik/dynamic/security.yaml +++ b/traefik/dynamic/security.yaml @@ -45,11 +45,15 @@ http: burst: 15 period: 1s # Only let traffic through whose source IP is inside the Tailscale/Headscale - # mesh (100.64.0.0/10, fd7a:115c:a1e0::/48). Public DNS can still resolve - # these hostnames, but anyone reaching them over the open internet gets a - # 403 — this range is only reachable by actually being a peer on the tailnet. + # mesh (100.64.0.0/10, fd7a:115c:a1e0::/48) or on falcon_network itself + # (172.18.0.0/16) - the latter so containers on our own internal docker + # network (e.g. pulsenode calling this router directly to reach + # api@internal) count as trusted the same way a tailnet peer does. Public + # DNS can still resolve these hostnames, but anyone reaching them over the + # open internet gets a 403. vpn-only: ipAllowList: sourceRange: - "100.64.0.0/10" - "fd7a:115c:a1e0::/48" + - "172.18.0.0/16"