Skip to content

Network Security

This page covers the network-facing controls: what’s exposed, TLS at the edge, optional internal mTLS, and the SSRF protections on outbound calls.

Only the proxy is public

In a correct deployment, only Traefik is reachable from the internet. Everything else — control plane, Postgres, Valkey, MinIO, realtime, Deno — sits on the internal Docker/Kubernetes network and is reached only through the gateway.

  • Don’t publish the internal service ports to the host in production.
  • Put Postgres, Valkey, and MinIO on a private network/subnet.
  • Restrict direct database access at the firewall; applications should go through the control plane (or Supavisor for pooled direct connections from trusted services only).

TLS at the edge

Traefik terminates TLS and can fetch certificates automatically via Let’s Encrypt. For production:

  1. Point your domain at the host and configure Traefik’s ACME resolver.
  2. Serve the control plane and console over HTTPS only.
  3. Disable the Traefik dashboard in production — set TRAEFIK_DASHBOARD_PORT to a closed/non-standard port, or remove it. It exposes routing internals.
Terminal window
# .env — production posture
TRAEFIK_HTTPS_PORT=443
# remove/relocate the dashboard:
# TRAEFIK_DASHBOARD_PORT=... (do not expose publicly)

Internal mTLS (defense in depth)

AnvilBase can require mutual TLS between internal services so that even on the internal network, services authenticate each other — on both legs: listeners require client certs AND every internal client presents its own service certificate. Off by default; enable for production:

Terminal window
docker/mtls/generate-certs.sh # CA + per-service certs
# .env:
ANVILBASE_MTLS_ENABLED=true

Certs mount read-only at /etc/anvilbase/tls/.... Rotate them on a schedule; re-run the generator and roll services.

See Internal mTLS for the complete internal edge map (which edges are mutually authenticated, the deno server-auth-only gap, and what stays plaintext), per-service certificate requirements, and the CI smoke coverage. See also Encryption.

SSRF protection on webhooks

The webhooks service runs an SSRF guard at two points. On create/update/test it refuses target URLs whose host is a literal in a blocked range — loopback, RFC1918, CGNAT, or link-local (including the cloud metadata endpoint 169.254.169.254). Because a hostname that resolved to a public IP at create time can be re-pointed at an internal IP afterwards (DNS rebinding), the delivery service also re-validates the resolved IP at dispatch time: it resolves the target host, drops any address in a blocked range, and refuses to connect if no public address remains. The connection is pinned to the validated address, so the resolve-then-connect race is closed. A blocked target makes the test call return success:false with the block reason, and a blocked delivery is recorded as a failed attempt (never silently dropped). The single env flag ALLOW_INTERNAL_WEBHOOK_TARGETS=true relaxes both checks for local/dev compose stacks. See Webhooks.

Credential brute-force & OTP/send abuse guards

Two layers protect the auth surface from credential stuffing, OTP brute-force, and SMS/email bombing.

Per-IP guard (control plane). A top-level middleware enforces a per-IP request budget on the credential surface, by route class (per 1-minute window, using the spoof-resistant client IP from ANVILBASE_TRUSTED_PROXIES):

ClassEndpointsBudget (per IP)
loginPOST /api/v1/platform/login10 / min
auth-credentialGoTrue token, signup10 / min
auth-verifyGoTrue verify (GET+POST), factors/{id}/verify10 / min
auth-sendGoTrue otp, magiclink, resend, recover5 / min

The tighter auth-send budget is because each send costs money (Twilio SMS) or floods a victim inbox. Over-budget returns 429 with Retry-After.

Per-recipient send cap (auth facade). The per-IP guard stops a single source; a distributed attacker (many IPs, one victim) is stopped by a per-recipient send cap on /otp, /magiclink, /resend, /recover (Postgres-backed, env OTP_EMAIL_SEND_MAX / OTP_SMS_SEND_MAX / OTP_SEND_WINDOW_SECS — see Configuration). Over-limit returns 429 over_email_send_rate_limit / over_sms_send_rate_limit with Retry-After, matching Supabase.

No user-enumeration. The send-cap 429 is keyed only on the supplied recipient identifier, so it fires identically for an existing and a non-existing recipient — it reveals that this identifier was targeted, never whether an account exists. A successful send still returns 200 {}.

Edge function egress

Edge functions reach the outside world over fetch. Treat their egress as a trust boundary: functions run your code but in a sandboxed Deno runtime with no ambient filesystem/network trust. For sensitive deployments, constrain outbound network access at the container/network policy level so a function can only reach the destinations it needs.

IP allow-lists and direct DB access

For direct Postgres access (analytics tools, trusted services), restrict by IP at the network layer (firewall / security group) and require SSL on the connection. Keep the application path (control plane) as the default and direct DB access as the deliberate exception, scoped to known sources.

The management plane is fail-closed

/api/v1/* rejects any request without a valid admin token or PAT (401). There’s no anonymous management surface. Protect those credentials accordingly — they’re deployment-admin-equivalent (RBAC).

Production network checklist

  • Only Traefik exposed publicly; internal ports unpublished.
  • TLS enforced at the edge; HTTP redirects to HTTPS.
  • Traefik dashboard not reachable from the internet.
  • Postgres/Valkey/MinIO on a private network; direct DB access IP-restricted.
  • ANVILBASE_MTLS_ENABLED=true (or an equivalent service-mesh mTLS).
  • Webhook SSRF guard relied upon (don’t disable it).
  • Management credentials (PATs/admin token) tightly held and rotated.

See the full Production Checklist.

Next: eIDAS 2.0 → Overview.