Currently Available: Need a skilled Software Developer for your next project?
Categories
Self-Hosting

How to Protect Self-Hosted Services With mTLS Client Certificates in Caddy

If you run a self-hosted service behind Caddy, you can require each connecting device to prove its identity with a certificate. Mutual TLS, or mTLS, adds that check to HTTPS: Caddy presents its usual server certificate, and the client must present a separate certificate that Caddy trusts.

Configure Caddy’s client_auth setting with a trust pool containing the certificate authority (CA) that issued your client certificates. A CA is the issuer whose certificate Caddy trusts when checking client certificates. This setup works well when an entire hostname should be private. It does not, by itself, decide what an authenticated user is allowed to do inside the service.

Configure Caddy to verify client certificates

Caddy checks the client certificate during the TLS handshake, the initial exchange that establishes an encrypted connection. A trust pool is a collection of trusted CA certificates that Caddy checks to confirm a client certificate came from an authorized issuer.

For a service whose requests Caddy forwards to another server, use a CA certificate file in PEM format:

service.example.com {
    tls {
        client_auth {
            mode require_and_verify
            trust_pool file /etc/caddy/client-ca.pem
        }
    }

    reverse_proxy 127.0.0.1:8080
}

Replace the hostname and backend address with your own. Put the CA certificate, not a client’s private key, in /etc/caddy/client-ca.pem. Make sure the Caddy process can read the file and that unauthorized users cannot modify it. If Caddy runs in a container, mount the CA file inside the container at the configured path.

The require_and_verify mode requires a client certificate and checks it against the configured trust pool. Caddy documents four client-authentication modes:

Mode What Caddy does
request Requests a certificate but allows a connection without one. It does not verify a supplied certificate.
require Requires a certificate but does not verify it.
verify_if_given Allows a connection without a certificate and verifies one if the client supplies it.
require_and_verify Requires a certificate and verifies it.

Caddy uses require_and_verify by default when a trust pool is configured. Writing it explicitly makes the intended access rule clear. The Caddy tls directive documents these modes and other trust-pool options.

Caddy’s HTTPS certificate and its client-authentication trust pool serve different purposes. Automatic HTTPS handles the certificate Caddy presents to clients; client_auth checks the certificate clients present to Caddy. Clients still need to trust Caddy’s server certificate. Caddy’s Automatic HTTPS documentation also notes that installing its internal CA certificate in trust stores is not guaranteed in containers or unprivileged deployments.

Choose who the CA trusts

A CA trust pool accepts a client certificate when its chain leads to a CA in the pool, so it trusts issuers rather than maintaining an allowlist of individual devices. To limit which certificates Caddy trusts, use a dedicated CA for client certificates instead of a broad issuer.

Issue a separate certificate and private key for each device or client. Keep each private key under that client’s control, and distribute it through a secure channel. Because each device or client has separate credentials, you can replace one client’s certificate without sharing a common private key with every device. Caddy also documents a leaf verifier for accepting a defined set of specific certificates.

Plan how you will issue, renew, replace, and remove client credentials. A basic CA trust-pool configuration does not provide a complete certificate-revocation process. Caddy supports custom verifier modules for checks such as revocation, but the appropriate setup depends on how you issue and manage certificates.

Protect the whole hostname

Caddy applies client_auth at the TLS connection level, before it handles an HTTP request. Configure a dedicated hostname when every request to that hostname should require a client certificate. If only one path should require mTLS while other paths remain public on the same hostname, use a design that separates those boundaries, such as a separate hostname. Caddy’s request matcher documentation describes request-level matching, which occurs after the TLS connection is established.

Caddy automatically enables strict_sni_host when client authentication is configured. This setting requires the HTTP Host header to match the hostname the client sends in Server Name Indication (SNI), the part of the TLS setup that tells Caddy which hostname the client wants. The check helps prevent a client from establishing a connection for one hostname and then requesting another protected hostname over that connection, as described in Caddy’s global options.

This inbound configuration authenticates clients connecting to Caddy. Caddy’s separate tls_client_auth option for reverse_proxy makes Caddy present a certificate when it connects to an HTTPS backend.

Test valid and rejected connections

Use a client certificate and private key to test a successful connection. Then test without a certificate and with one issued by an untrusted CA. For example, if the client certificate and key are PEM files:

curl --cert client.pem --key client-key.pem https://service.example.com/

If the request succeeds with the trusted certificate, Caddy accepted that credential, though the service’s authorization checks still apply. A request without a certificate or with a certificate from an untrusted CA should fail during TLS setup. Make sure the client also trusts Caddy’s server certificate so a server-certificate error does not obscure the client-authentication result.

Use a patched Caddy release

Check the Caddy binary and configuration you actually deploy. Caddy’s security advisory says v2.10.2 could accept certificates it should reject when the configured client CA file was missing or unreadable. The advisory says the same could happen when the file was malformed. In that affected release, Caddy could accept client certificates against system-trusted roots instead of the intended private CA. The advisory identifies v2.11.0 as patched.

As of November 9, 2026, the Caddy release history lists v2.11.9, released November 5, 2026. Use a patched release and keep tests that confirm Caddy rejects missing or untrusted client certificates. Also verify that the configured CA file exists, is readable by Caddy, and contains the expected certificate. The security advisory explains the affected behavior and patched versions.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *