Skip to content

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

  1. DNS: Add internal.*.eva-00.network as a local DNS record pointing to Caddy (192.168.1.200). Do NOT add it to Cloudflare — external DNS won't resolve it.
  2. Caddy: Add internal.<service>.eva-00.network routes. Use @blocked matcher 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
    }
    
  3. NetBird routes: Ensure NetBird advertises the 192.168.1.0/24 subnet so VPN clients can reach internal services.
  4. 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:

  1. Wildcard cert covers it: If the cert is *.eva-00.network, it covers internal.vault.eva-00.network (single subdomain). But internal. prefix makes it internal.vault.eva-00.network which is two levels deep — wildcard doesn't cover this.
  2. Separate cert with DNS challenge: Add internal.*.eva-00.network to Cloudflare as a record (even if it points to a private IP), and request a cert for it.
  3. 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.