Root cause of the provision-time 504s: netbird home-lab-network (192.168.8.0/24) had no active routing peer — mac-mini routing peer's netbird daemon was down, so all home-backed public services (artifacto/blog/trmnl) 504'd at the VPS edge. netbird up on mac-mini restored it; verified trmnl public 200/401, artifacto 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.1 KiB
Public ingress — VPS traefik + cert mirror
How home services reach the open internet without exposing the home network. Two-stage pattern: traefik on the IONOS VPS terminates TLS at the public edge, then reverse-proxies over the netbird mesh to home Caddy / direct backends.
The shape
Public client
│ *.hubris.network → 82.165.190.79 (IONOS wildcard)
▼
[VPS traefik] priority 1: HostSNI(*) → netbird-proxy:8443 ← netbird control plane
priority 10: per-host HTTP routers ← home services
│ HTTP over netbird mesh
▼
[Home backend on 192.168.8.x]
LAN clients resolve via the Technitium DNS on dns (107) → 192.168.8.175 → home Caddy (121), unchanged. The two paths are independent.
Why this shape
- VPS traefik already has a HostSNI(*) TCP passthrough at priority 1 so netbird's own ingress (
netbird.hubris.network, future*.proxy.hubris.network) is unaffected. - Per-hostname HTTP file-provider routers at priority 10 win over the passthrough for the listed hosts and let traefik terminate TLS itself for those.
- Traefik's own ACME (
letsencryptresolver) fails on this box: HostSNI(*) grabs TLS-ALPN-01 challenges before traefik'sallowACMEByPass=truecan respond. Solution: home Caddy obtains certs via IONOS DNS-01 (no conflict) and the VPS mirrors the result over.
Components
On the VPS (82.165.190.79)
/opt/traefik-dynamic.yaml— file-watched dynamic config. Onehttp.routers.<name>-public+ onehttp.services.<name>-publicper exposed service, plus one entry in the top-leveltls.certificateslist per hostname.- Let's-Encrypt-effective directory:
/letsencrypt/inside the traefik container, backed by the host-side docker volumeopt_netbird_traefik_letsencrypt. - Backups before edits:
cp /opt/traefik-dynamic.yaml /opt/traefik-dynamic.yaml.bak.$(date +%s)— several bak files live alongside.
On the PVE host (192.168.8.77)
/usr/local/bin/hubris-public-cert-sync.sh— runs daily viahubris-public-cert-sync.timer. Maps source hostname → VPS cert filenames in a bash assoc array. For each mapping:pct pullcert+key from Caddy (121)'s store, diff against the VPS copy, scp only on change.- Filenames are stable per host so the dynamic.yaml never needs editing on renewal — traefik file-watches and hot-reloads the cert.
Services currently exposed
| Hostname | Path scope | Backend | Middlewares | Cert files on VPS |
|---|---|---|---|---|
artifacto.hubris.network |
/p/*, /static/*, /healthz |
192.168.8.205:3100 |
artifacto-strip-sso + artifacto-ratelimit (50 rps / 100 burst) |
fullchain.crt / privkey.key |
blog.hubris.network |
whole host | 192.168.8.205:8080 |
blog-ratelimit (100 rps / 200 burst) |
blog.fullchain.crt / blog.privkey.key |
trmnl.hubris.network |
whole host | 192.168.8.211:9851 (trmnl 128) |
trmnl-ratelimit (20 rps / 40 burst) |
trmnl.fullchain.crt / trmnl.privkey.key |
artifacto-strip-sso blanks inbound X-Authentik-* and X-Artifacto-Gateway so external clients can't spoof the SSO auto-login header contract. Path split is enforced at the VPS router rule, not by home Caddy. See Artifacto on apps (105).
auth.hubris.network — different pattern (local container, not cert-mirror)
Since 2026-05-31 Authentik runs on the VPS itself, so auth.hubris.network is served by a local Docker container, not proxied to a home backend. It therefore does not use the file-provider + cert-mirror pattern above:
- Routed via traefik Docker provider labels on the
authentik-serverservice (/opt/docker-compose.yml), nottraefik-dynamic.yaml. - TLS via traefik's own
letsencryptresolver (works here because it's a normal HTTP router, not the HostSNI passthrough). - Traefik reaches it over the
authDocker network (172.30.1.0/24); Postgres/Redis on that net are isolated from the netbird containers. - Admin UI is IP-gated: an
admin-allowlistipAllowList middleware onPathPrefix(/if/admin/)(currently5.61.168.0/24). Login/flow endpoints stay public.
No cert-mirror entry and no hubris-public-cert-sync.sh mapping is needed for auth.
Recipe — exposing another service
- Ensure home Caddy on LXC 121 already serves the hostname (cert exists at
/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/<host>/). - Add an entry to
HOSTSin/usr/local/bin/hubris-public-cert-sync.shmapping the hostname → VPS filenames. Run once:systemctl start hubris-public-cert-sync.service. Confirm the cert landed. - Edit
/opt/traefik-dynamic.yamlon the VPS:- Add to
tls.certificates: paths/letsencrypt/<crt>and/letsencrypt/<key>. - Add
http.routers.<host>-public:rule: 'Host(\`)'(or with path matchers if scope-gating),entryPoints: [websecure],priority: 10,tls: {},service: -public,middlewares: [...]`. - Add a ratelimit middleware under
http.middlewaresif wanted. - Add
http.services.<host>-public.loadBalancer.servers[0].url: 'http://<lan-ip>:<port>'.
- Add to
- Verify:
ssh root@100.122.165.149 'curl -skI --resolve <host>:443:127.0.0.1 https://<host>/' # 2xx/3xx curl -skI --resolve <host>:443:<vps-public-ip> https://<host>/ # same - No DNS edit needed — the IONOS wildcard already points at the VPS.
What does NOT follow this pattern
netbird.hubris.network(and any future*.proxy.hubris.network) uses the netbird-proxy / HostSNI passthrough path. Netbird handles its own cert via ACME cleanly because it is the passthrough target.
Related
- DNS split-horizon
- Caddy (121) — cert source, internal counterpart
- Mesh migration — netbird is the transport between VPS and home
- VPS hardening — fail2ban / nftables that the access logs feed
- Artifacto on apps (105) — first publicly-exposed service
Changelog
2026-06-24 — trmnl.hubris.network exposed
TRMNL plugins middleware on trmnl (128). File-provider router trmnl-public → 192.168.8.211:9851, trmnl-ratelimit (20 rps / 40 burst), cert mirrored as trmnl.fullchain.crt/trmnl.privkey.key. Verified live from the internet (200 with token / 401 without). It was provisioned during a mesh outage — the home-lab-network (192.168.8.0/24) route had no active routing peer because the mac-mini routing peer's netbird was down (all home-backed public services 504'd). Bringing netbird up on mac-mini restored the route; no traefik change was needed.
2026-05-31 — auth.hubris.network now served locally on the VPS
Authentik migrated onto the VPS (investigation). Unlike the home-backed services above, auth is a local container routed via traefik Docker-provider labels with traefik-managed Let's Encrypt — no cert-mirror, no traefik-dynamic.yaml router. Admin UI gated by an ipAllowList middleware. Traefik gained a second Docker network (auth, 172.30.1.0/24) to reach it while keeping its DB/Redis isolated from the netbird stack.
2026-04-28 — wiki entry created
Initial documentation.
2026-04-23 — blog.hubris.network exposed
WriteFreely on apps (105). Whole host is public.
2026-04-22 — pattern established with Artifacto
First service through the file-provider router. IONOS wildcard moved to the VPS this day. Cert mirror script + timer deployed on the PVE host.