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.
falcon-vpn-only was defined inline in falcon.yaml even though it's a
generic ipAllowList restriction with nothing falcon-specific about it
- security.yaml is where the other reusable middlewares (security-
headers, no-index, rate-limit) already live. Renamed to vpn-only to
match their bare naming style now that it's not tied to one file, and
updated falcon.yaml's two router references to vpn-only@file (same
cross-file pattern already used there for security-headers@file).
Also applies it to the pulsenode stack: it's a dashboard surfacing
container names/health for every service here, and there's no auth in
front of it yet, so gating it to the tailnet is a real mitigation for
that gap rather than leaving it open on the public hostname.
Migrated 11 services from monolithic docker-compose project into independent stacks,
each with dedicated databases, minimal .env configuration, and bind-mount data volumes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>