Memory
Architecture decision record 0003 accepted

Terminate TLS at a single reverse proxy

One ingress point holds TLS, security headers, and the authentication boundary. Services publish no host ports.

  • Security
  • TLS
  • Ingress

2 min read · 467 words

Context01

The forces at the time

Self-hosted services default to publishing a host port each. Left alone, that produces a set of plaintext endpoints with independent and inconsistent authentication, reachable by anything that can route to the host.

The network in question is not a trusted environment. It carries IoT devices, guest machines, and appliances that are rarely updated. "It is only local" is an assumption about that population, and it is not a safe one.

Configuring TLS per application is not viable either: each one supports it differently, some not at all, and certificate renewal would have as many implementations as there are services.

Decision02

What was chosen

Run Caddy as the sole ingress. Services attach to a shared container network with the proxy and publish no host ports; upstreams are addressed by container name. TLS is terminated at the proxy, security headers are applied there, and the authentication boundary sits at the same point.

Certificates for internal names are obtained through a DNS challenge, producing publicly-trusted certificates for names that resolve only on the internal network. Self-signed certificates with clicked-through browser warnings are explicitly not acceptable, because normalising that warning removes the signal it exists to provide.

Alternatives03

What else was considered

Per-service TLS
Rejected. As many certificate configurations and renewal mechanisms as there are services, several of which do not support TLS at all.
Plaintext on the local network
Rejected. Assumes every device on the segment is trustworthy, which is precisely what an IoT-carrying network is not.
Self-signed certificates
Rejected. Encrypts the transport but trains the operator to dismiss certificate warnings, which is a worse long-term outcome than the problem it solves.
A private certificate authority
Viable and nearly chosen. Rejected on operational cost: a root to distribute to every client and to rotate. The DNS challenge achieves trusted certificates with no client-side distribution.
A VPN with no ingress at all
Rejected as a replacement, kept as a complement. It controls who reaches the host but does nothing about what happens after they do.
Consequences04

What follows

Gained

  • TLS, security headers, and access logging are configured once and apply to every service.
  • No service is reachable except through the proxy; there are no host ports to scan.
  • Services are addressed by name and path rather than by remembered port numbers.
  • A single access log makes request review practical rather than theoretical.

Paid for

  • The proxy is a single point of failure: if it is down, everything is down.
  • Proxy-to-service traffic is plaintext on the container network. Acceptable on one host; not acceptable once those hops cross a physical link.
  • The DNS challenge requires a real domain and API credentials for its DNS provider, which is itself a secret to protect.
  • A proxy misconfiguration is a systemic exposure rather than a single-service one.

Records are immutable once accepted. A change supersedes this one rather than editing it.