TLS Termination
ACME strictly expects traffic over HTTPS (RFC 8555 §6.1).
While you can place acme-proxy behind a reverse proxy (like Nginx, HAProxy, or
Traefik) and let it terminate TLS, acme-proxy is fully capable of terminating
TLS itself via the highly secure rustls crate.
Architectural security (Slowloris protection)
Terminating TLS in async frameworks requires care to avoid denial-of-service
vectors. In acme-proxy, handshakes run off the accept path. When a TCP
connection arrives, a background task spawns the TLS handshake into a bounded
channel. The handshake is strictly bounded by handshake_timeout_ms. If this
were done inline, a single stalled client (e.g., a Slowloris attack) could block
every other connection for the length of the timeout.
Client IP preservation
Under the hood, acme-proxy wraps the TLS listener in a TapIo struct. This is
load-bearing. Without this wrapper, the Axum HTTP layer would lose visibility
into the underlying TCP socket’s peer address once TLS is wrapped around it. By
using TapIo, acme-proxy ensures that the allowed_ip and reverse_dns
filters can correctly identify the true client IP, failing closed if it cannot
be determined.
Configuration
To serve HTTPS directly, configure the [server.tls] section.
Note: Setting
server.tls.enabled = truereplaces the cleartext HTTP listener entirely. You do not get both HTTP and HTTPS simultaneously.
[server]
base_url = "https://acme.internal:3000"
bind_address = "[::]:3000"
[server.tls]
enabled = true
# The PEM encoded certificate chain (leaf first) and private key
cert_path = "server.pem"
key_path = "server.key"
# Budget for one TLS handshake
handshake_timeout_ms = 10000
If cert_path or key_path files are missing on disk at startup, acme-proxy
will automatically generate a self-signed certificate for the host specified
in server.base_url and write it to disk.
The web admin listener
[admin.tls] is the same mechanism on a second socket: the same
load-or-generate provisioning, the same TapIo wrapper preserving the peer
address, the same 0600 on a generated key. Only the defaults differ.
[admin.tls]
enabled = true
cert_path = "admin.pem" # not server.pem
key_path = "admin.key"
The paths are separate on purpose: the two listeners answer to different names
(admin.base_url versus server.base_url, and a generated certificate takes
its name from whichever applies), and sharing one certificate between them
should be a decision, not an accident. The log lines carry a listener field
("acme" or "admin") so certificate churn on one is distinguishable from the
other.
Unlike the ACME listener, TLS here is not optional once the panel leaves loopback — startup refuses that combination outright. See Web Admin.