ADR-008 — Internal-Only URLs for Critical Services (VPN)
Date: 2026-04-10 Status: Proposed
Decision
Critical infrastructure services should only be accessible via VPN (NetBird). These services get internal.<service>.eva-00.network URLs that resolve only within the VPN/LAN, with Caddy rejecting external access.
Context
All services currently share the same *.eva-00.network wildcard DNS, routed through Cloudflare to Caddy. This means services like PBS, Vault, Grafana, and PocketID are reachable from the public internet (behind Caddy TLS + optional oauth2-proxy). While SSO protects most services, the attack surface is unnecessarily large for admin/infrastructure tools that are only used from home or via VPN.
Proposed architecture
URL scheme
| URL pattern | Access | Use case |
|---|---|---|
<service>.eva-00.network |
Public (via Cloudflare) | User-facing services (Karakeep, Jellyfin, Paperless, etc.) |
internal.<service>.eva-00.network |
VPN/LAN only | Infrastructure services (Vault, PBS, Grafana, PocketID, etc.) |
Critical services (move to internal-only)
| Service | Current URL | New URL |
|---|---|---|
| Vault | vault.eva-00.network |
internal.vault.eva-00.network |
| PBS | pbs.eva-00.network |
internal.pbs.eva-00.network |
| Grafana | grafana.eva-00.network |
internal.grafana.eva-00.network |
| PocketID | auth.eva-00.network |
Keep public (SSO provider for all services) |
| Databasement | databasement.eva-00.network |
internal.databasement.eva-00.network |
| Forgejo | git.eva-00.network |
internal.git.eva-00.network |
| Ntfy | ntfy.eva-00.network |
Keep public (push notifications to mobile) |
| Glance | glance.eva-00.network |
internal.glance.eva-00.network |
PocketID stays public — it's the SSO provider. If it's internal-only, external services can't complete OIDC flows. Ntfy stays public — mobile push notifications need external access.
Implementation
- DNS: Add
internal.*.eva-00.networkas a local DNS record pointing to Caddy (192.168.1.200). Do NOT add it to Cloudflare — external DNS won't resolve it. - Caddy: Add
internal.<service>.eva-00.networkroutes. Use@blockedmatcher to reject requests not from LAN/VPN subnets:internal.vault.eva-00.network { @blocked not remote_ip 192.168.1.0/24 100.64.0.0/10 respond @blocked 403 reverse_proxy 192.168.1.106:8200 } - NetBird routes: Ensure NetBird advertises the
192.168.1.0/24subnet so VPN clients can reach internal services. - Transition period: Keep old public URLs working but redirect to internal URLs with a warning. Remove public routes after confirming VPN access works for all users.
TLS for internal domains
Caddy uses Let's Encrypt with Cloudflare DNS challenge for *.eva-00.network. For internal.* domains (not in Cloudflare DNS), options:
- Wildcard cert covers it: If the cert is
*.eva-00.network, it coversinternal.vault.eva-00.network(single subdomain). Butinternal.prefix makes itinternal.vault.eva-00.networkwhich is two levels deep — wildcard doesn't cover this. - Separate cert with DNS challenge: Add
internal.*.eva-00.networkto Cloudflare as a record (even if it points to a private IP), and request a cert for it. - Internal CA: Use Caddy's internal CA for internal domains. Requires trusting the CA on all clients.
Recommended: Option 2 — add *.internal.eva-00.network or individual records to Cloudflare pointing to Caddy's LAN IP. Cloudflare DNS challenge works regardless of whether the IP is public.
Alternative URL scheme: vault.internal.eva-00.network instead of internal.vault.eva-00.network. This allows a *.internal.eva-00.network wildcard cert and keeps the subdomain structure cleaner.
Trade-offs
Accepted
- Requires VPN for admin access: When away from home, must connect to NetBird first. Acceptable — admin tasks shouldn't be done from untrusted networks anyway.
- More complex DNS setup: Two zones (public + internal) instead of one. Manageable with Cloudflare + local DNS.
- OIDC callback URLs change: Services using PocketID need their redirect URIs updated to the new internal URLs.
Rejected
- IP allowlisting only (no URL change): Caddy can restrict by source IP, but keeping the same public URL means DNS still resolves externally. Attackers can probe the endpoint even if blocked. A non-resolving domain is better.
- Cloudflare Access / Zero Trust: Adds external dependency and complexity. NetBird is already deployed and working.