Caddy SSL/TLS Configuration

Caddy is a modern web server with automatic HTTPS by default. It obtains and renews TLS certificates from Let's Encrypt and ZeroSSL without any configuration, making it one of the simplest ways to serve secure websites.

Automatic HTTPS (Default Behavior)

Basic site with automatic TLS

Caddy automatically obtains and renews certificates when you use a qualifying domain name. No TLS configuration is needed:

example.com {
    root * /var/www/html
    file_server
}

Caddy will automatically obtain a certificate from Let's Encrypt or ZeroSSL, serve the site over HTTPS on port 443, and redirect HTTP to HTTPS on port 80.

Multi-domain automatic TLS

Serve multiple domains with automatic certificates for each:

example.com {
    root * /var/www/example
    file_server
}

www.example.com {
    redir https://example.com{uri}
}

api.example.com {
    reverse_proxy localhost:8080
}

Each site block with a qualifying domain automatically gets its own certificate. Caddy batches requests to the CA when possible.

Multiple domains in one site block

A single site can handle multiple domain names with one SAN certificate:

example.com, www.example.com {
    root * /var/www/html
    file_server
}

Wildcard certificates with DNS challenge

Wildcard certificates require the DNS challenge. You must configure a DNS provider module:

*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    @app host app.example.com
    handle @app {
        reverse_proxy localhost:3000
    }

    @api host api.example.com
    handle @api {
        reverse_proxy localhost:8080
    }

    handle {
        respond "Not Found" 404
    }
}

Caddy must be built with the appropriate DNS provider plugin (e.g., caddy-dns/cloudflare). Use xcaddy build --with github.com/caddy-dns/cloudflare to build with the plugin.

Automatic HTTPS redirect behavior

Caddy automatically creates HTTP-to-HTTPS redirects. You can customize this behavior:

# Automatic HTTPS is enabled by default for qualifying domains
# To disable automatic HTTPS entirely:
{
    auto_https off
}

# To disable only HTTP-to-HTTPS redirects (keep auto certs):
{
    auto_https disable_redirects
}

# To disable certificate management only (keep redirects):
{
    auto_https disable_certs
}

HTTP-only site (no TLS)

Use a protocol prefix or port to serve HTTP without automatic HTTPS:

# Serve on HTTP only using explicit protocol
http://example.com {
    root * /var/www/html
    file_server
}

# Or serve on a specific port (no auto HTTPS)
:8080 {
    respond "Hello, HTTP!"
}

TLS Directive Configuration

Specify certificate and key files

Use your own certificate and private key instead of automatic ACME:

example.com {
    tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
    reverse_proxy localhost:8080
}

The certificate file should include the full chain (leaf + intermediates). Providing your own certificate disables automatic ACME issuance for that site.

Specify ACME email address

Set the email address used for ACME account registration:

example.com {
    tls [email protected]
    reverse_proxy localhost:8080
}

# Or set it globally for all sites:
{
    email [email protected]
}

example.com {
    reverse_proxy localhost:8080
}

Internal (self-signed) certificates

Use Caddy's internal CA for local development or internal services:

# Use Caddy's built-in CA for self-signed certificates
localhost {
    tls internal
    respond "Hello from local HTTPS!"
}

# Internal TLS with specific options
intranet.local {
    tls internal {
        on_demand
    }
    reverse_proxy localhost:3000
}

Run caddy trust to install Caddy's root CA certificate into your system trust store so browsers accept these certificates.

TLS protocol versions

Restrict the minimum and maximum TLS protocol versions:

example.com {
    tls {
        protocols tls1.2 tls1.3
    }
    reverse_proxy localhost:8080
}

# TLS 1.3 only (maximum security)
secure.example.com {
    tls {
        protocols tls1.3
    }
    reverse_proxy localhost:9090
}

The first argument is the minimum version, the second is the maximum. Supported values: tls1.2 and tls1.3. Caddy does not support TLS 1.0 or 1.1.

Cipher suite configuration

Specify allowed cipher suites for TLS 1.2 connections (TLS 1.3 cipher suites are not configurable):

example.com {
    tls {
        ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
    }
    reverse_proxy localhost:8080
}

Caddy uses Go's standard TLS cipher suite names. Only TLS 1.2 cipher suites can be configured; TLS 1.3 suites are always enabled and cannot be changed.

Curve preferences

Configure the elliptic curves used for key exchange:

example.com {
    tls {
        curves x25519 secp256r1 secp384r1
    }
    reverse_proxy localhost:8080
}

Supported curves: x25519, secp256r1, secp384r1, secp521r1. The order specifies preference.

Certificate key type

Specify the type of key to use when generating CSRs for ACME:

example.com {
    tls {
        key_type p256
    }
    reverse_proxy localhost:8080
}

Supported key types: ed25519, p256, p384, rsa2048, rsa4096.

ACME Configuration

Custom ACME CA

Use a different ACME Certificate Authority instead of the defaults:

# Per-site ACME CA
example.com {
    tls {
        issuer acme {
            dir https://acme.example.com/directory
        }
    }
    reverse_proxy localhost:8080
}

# Global ACME CA for all sites
{
    acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

Use the Let's Encrypt staging URL during testing to avoid hitting production rate limits.

DNS challenge provider

Configure the DNS challenge for certificates (required for wildcard certs and useful when ports 80/443 are unavailable):

# Cloudflare DNS challenge
example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy localhost:8080
}

# Route53 DNS challenge
example.com {
    tls {
        dns route53 {
            region us-east-1
            access_key_id {env.AWS_ACCESS_KEY_ID}
            secret_access_key {env.AWS_SECRET_ACCESS_KEY}
        }
    }
    reverse_proxy localhost:8080
}

# DNS challenge with propagation settings
example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
        resolvers 1.1.1.1
        propagation_timeout 120s
    }
    reverse_proxy localhost:8080
}

DNS provider modules must be compiled into Caddy. Build a custom binary using xcaddy with the required plugin.

ACME issuer with full options

Fine-grained control over the ACME issuer configuration:

example.com {
    tls {
        issuer acme {
            dir https://acme-v02.api.letsencrypt.org/directory
            email [email protected]
            timeout 30s
            disable_http_challenge
            disable_tlsalpn_challenge
            alt_http_port 5080
            alt_tlsalpn_port 5443
            preferred_chains {
                root_common_name "ISRG Root X1"
            }
        }
    }
    reverse_proxy localhost:8080
}

ZeroSSL issuer

Explicitly use ZeroSSL as the certificate authority:

example.com {
    tls {
        issuer zerossl {env.ZEROSSL_API_KEY}
    }
    reverse_proxy localhost:8080
}

On-demand TLS

Obtain certificates at the time of the first TLS handshake, rather than at config load. Useful for serving many domains dynamically:

# Global on-demand TLS settings (required)
{
    on_demand_tls {
        ask http://localhost:5555/check
        interval 2m
        burst 5
    }
}

# Site using on-demand TLS
https:// {
    tls {
        on_demand
    }
    reverse_proxy localhost:8080
}

The ask endpoint must return HTTP 200 to allow certificate issuance for a domain. This prevents abuse. The interval and burst settings rate-limit on-demand issuance.

Client Certificate Authentication (mTLS)

Require client certificates

Configure mutual TLS by requiring clients to present a valid certificate:

example.com {
    tls {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/ssl/client-ca.pem
        }
    }
    reverse_proxy localhost:8080
}

Client authentication modes

Caddy supports multiple client certificate verification modes:

# Require and verify (strictest - default when client_auth is used)
tls {
    client_auth {
        mode require_and_verify
        trusted_ca_cert_file /etc/ssl/client-ca.pem
    }
}

# Request but don't require (optional client certs)
tls {
    client_auth {
        mode request
    }
}

# Verify if given (optional, but verify if presented)
tls {
    client_auth {
        mode verify_if_given
        trusted_ca_cert_file /etc/ssl/client-ca.pem
    }
}

# Require any certificate (no verification against CA)
tls {
    client_auth {
        mode require
    }
}

Multiple trusted CA certificates

Trust multiple CA certificates for client authentication:

example.com {
    tls {
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/ssl/ca1.pem
            trusted_ca_cert_file /etc/ssl/ca2.pem
        }
    }
    reverse_proxy localhost:8080
}

Leaf certificate matching

Trust specific leaf (client) certificates directly instead of a CA:

example.com {
    tls {
        client_auth {
            mode require_and_verify
            trusted_leaf_cert_file /etc/ssl/allowed-client.pem
        }
    }
    reverse_proxy localhost:8080
}

Leaf certificate matching pins to specific client certificates. This is useful when you have a small, known set of clients and do not need a full CA infrastructure.

Certificate Management

Certificate storage location

Caddy stores certificates and ACME account data in a data directory. You can customize the storage backend:

# Default storage location:
#   Linux:  $HOME/.local/share/caddy  (or $XDG_DATA_HOME/caddy)
#   macOS:  $HOME/Library/Application Support/Caddy

# Custom file system storage
{
    storage file_system {
        root /var/lib/caddy/certificates
    }
}

Multiple certificate issuers

Configure fallback issuers in case the primary CA is unavailable:

example.com {
    tls {
        issuer acme {
            dir https://acme-v02.api.letsencrypt.org/directory
            email [email protected]
        }
        issuer zerossl {env.ZEROSSL_API_KEY}
    }
    reverse_proxy localhost:8080
}

Caddy will try issuers in the order they are defined. If the first issuer fails, it falls back to the next one.

Certificate automate via global options

Manage certificates for domains that are not served directly by Caddy:

{
    cert_issuer acme
}

# Caddy will obtain and renew certificates even for domains
# only referenced in reverse_proxy or other directives

Loading certificate and key files

Load certificates from files, including dual RSA and ECDSA certificates:

# Provide cert and key files directly
example.com {
    tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
}

OCSP Stapling

Default OCSP stapling behavior

Caddy enables OCSP stapling by default for all managed certificates:

# OCSP stapling is automatic - no configuration needed.
# Caddy will:
#   1. Fetch OCSP responses for all managed certificates
#   2. Cache OCSP responses and refresh before expiry
#   3. Staple OCSP responses to TLS handshakes
#   4. Replace revoked certificates automatically

example.com {
    reverse_proxy localhost:8080
}

Caddy checks OCSP status regularly. If a certificate is found to be revoked, Caddy will attempt to replace it automatically.

OCSP stapling with custom certificates

OCSP stapling also works with manually loaded certificates, as long as the OCSP responder URL is included in the certificate:

# Custom certs get OCSP stapling too, if the certificate
# contains an OCSP responder URL in its AIA extension
example.com {
    tls /etc/ssl/certs/example.com.pem /etc/ssl/private/example.com.key
    reverse_proxy localhost:8080
}

Verify OCSP stapling

Test that OCSP stapling is working correctly:

# Check OCSP stapling with OpenSSL
echo QUIT | openssl s_client -connect example.com:443 -status 2>&1 | grep -A 17 "OCSP response:"

# Expected output includes:
# OCSP Response Status: successful (0x0)
# Cert Status: good

Security Headers

HSTS (HTTP Strict Transport Security)

Add HSTS headers to instruct browsers to always use HTTPS:

example.com {
    header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    reverse_proxy localhost:8080
}

Note: Caddy already provides HTTPS by default. HSTS adds an extra layer by telling browsers to never attempt an unencrypted connection.

Comprehensive security headers

Add a full set of security headers for defense in depth:

example.com {
    header {
        # HSTS (2 years)
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

        # Prevent MIME type sniffing
        X-Content-Type-Options "nosniff"

        # Clickjacking protection
        X-Frame-Options "DENY"

        # Referrer policy
        Referrer-Policy "strict-origin-when-cross-origin"

        # Content Security Policy
        Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"

        # Permissions Policy
        Permissions-Policy "geolocation=(), microphone=(), camera=()"

        # Remove server identifier
        -Server
    }
    reverse_proxy localhost:8080
}

The -Server prefix removes the Server header from responses. Caddy's header directive can set, add, or delete response headers.

Security headers as a reusable snippet

Define security headers once and reuse them across multiple sites:

(security_headers) {
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        -Server
    }
}

example.com {
    import security_headers
    reverse_proxy localhost:8080
}

api.example.com {
    import security_headers
    reverse_proxy localhost:9090
}

Snippets are defined with parentheses and imported with the import directive. They are a powerful way to keep Caddyfiles DRY.

Reverse Proxy with TLS

Reverse proxy to HTTPS backend

Forward requests to an upstream that uses HTTPS:

example.com {
    reverse_proxy https://backend.internal:8443
}

When the upstream URL uses https://, Caddy automatically connects to it over TLS and verifies its certificate.

Skip backend certificate verification

Connect to an HTTPS backend with a self-signed or untrusted certificate:

example.com {
    reverse_proxy https://backend.internal:8443 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}

Use this only for development or when the backend certificate cannot be properly verified. In production, prefer configuring a trusted CA.

Custom CA for backend verification

Specify a custom root CA to verify the upstream's certificate:

example.com {
    reverse_proxy https://backend.internal:8443 {
        transport http {
            tls_trusted_ca_certs /etc/ssl/internal-ca.pem
        }
    }
}

Client certificate to backend (mTLS upstream)

Present a client certificate when connecting to the upstream:

example.com {
    reverse_proxy https://backend.internal:8443 {
        transport http {
            tls_client_auth /etc/ssl/client.pem /etc/ssl/client-key.pem
            tls_trusted_ca_certs /etc/ssl/internal-ca.pem
        }
    }
}

TLS server name override for backend

Override the server name used for TLS verification when the backend hostname differs from its certificate:

example.com {
    reverse_proxy https://10.0.0.5:8443 {
        transport http {
            tls_server_name backend.example.com
        }
    }
}

Complete Caddyfile Examples

Production Caddyfile with hardened TLS

A complete production-ready Caddyfile with TLS best practices:

# Global options
{
    email [email protected]
}

# Security headers snippet
(security_headers) {
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        -Server
    }
}

# Main site
example.com {
    tls {
        protocols tls1.2 tls1.3
        curves x25519 secp256r1 secp384r1
    }
    import security_headers
    encode gzip zstd
    root * /var/www/example.com
    file_server
    log {
        output file /var/log/caddy/example.com.log
    }
}

# WWW redirect
www.example.com {
    redir https://example.com{uri}
}

# API subdomain
api.example.com {
    tls {
        protocols tls1.2 tls1.3
    }
    import security_headers
    reverse_proxy localhost:8080
    log {
        output file /var/log/caddy/api.example.com.log
    }
}

Multi-site configuration

Serve multiple sites with different configurations, each with automatic certificates:

{
    email [email protected]
}

# Marketing site - static files
marketing.company.com {
    root * /var/www/marketing
    file_server
    encode gzip
}

# Application - reverse proxy
app.company.com {
    reverse_proxy localhost:3000
}

# API - reverse proxy with CORS
api.company.com {
    @cors_preflight method OPTIONS
    handle @cors_preflight {
        header Access-Control-Allow-Origin "https://app.company.com"
        header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        header Access-Control-Allow-Headers "Content-Type, Authorization"
        respond "" 204
    }
    header Access-Control-Allow-Origin "https://app.company.com"
    reverse_proxy localhost:8080
}

# Admin panel - IP restricted
admin.company.com {
    @blocked not remote_ip 10.0.0.0/8
    respond @blocked "Forbidden" 403
    reverse_proxy localhost:9090
}

Wildcard site with DNS challenge

Full wildcard setup with DNS-based certificate validation:

*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    @blog host blog.example.com
    handle @blog {
        reverse_proxy localhost:2368
    }

    @shop host shop.example.com
    handle @shop {
        reverse_proxy localhost:3000
    }

    @docs host docs.example.com
    handle @docs {
        root * /var/www/docs
        file_server
    }

    # Default: not found
    handle {
        respond "Subdomain not configured" 404
    }
}

mTLS configuration

Complete mutual TLS setup for an internal service:

internal-api.example.com {
    tls {
        protocols tls1.3
        client_auth {
            mode require_and_verify
            trusted_ca_cert_file /etc/ssl/internal-ca.pem
        }
    }

    # Log client certificate info
    log {
        output file /var/log/caddy/mtls-access.log
        format json
    }

    reverse_proxy localhost:8080
}

Local development setup

Development configuration using Caddy's internal CA for local HTTPS:

# First run: caddy trust  (installs Caddy's root CA)

localhost {
    tls internal

    # Frontend
    handle /api/* {
        reverse_proxy localhost:8080
    }

    handle {
        reverse_proxy localhost:3000
    }
}

# Alternative: multiple local services
https://app.localhost {
    tls internal
    reverse_proxy localhost:3000
}

https://api.localhost {
    tls internal
    reverse_proxy localhost:8080
}

CLI Commands

Install CA certificate into system trust store

# Install Caddy's root CA certificate into your system trust store
# (requires admin/root privileges)
caddy trust

Required for browsers to trust certificates issued by Caddy's internal CA (used with tls internal).

Remove CA certificate from trust store

# Remove Caddy's root CA certificate from your system trust store
caddy untrust

# Untrust a specific CA certificate by file
caddy untrust --cert /path/to/ca-cert.pem

List managed certificates

# List all managed certificates (requires running Caddy instance)
caddy certificates

Validate Caddyfile syntax

# Convert Caddyfile to JSON and validate
caddy adapt --config /etc/caddy/Caddyfile

# Validate with pretty output
caddy adapt --config /etc/caddy/Caddyfile --pretty

# Validate and check for warnings
caddy validate --config /etc/caddy/Caddyfile

Always validate your Caddyfile before reloading. The adapt command converts the Caddyfile to Caddy's native JSON format, and validate checks the entire configuration.

Reload configuration

# Reload Caddy configuration gracefully (zero downtime)
caddy reload --config /etc/caddy/Caddyfile

# Reload with a specific adapter
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile

# Or send a SIGUSR1 signal
kill -USR1 $(pidof caddy)

Reloading is graceful: existing connections continue to be served while new connections use the updated configuration.

Run Caddy

# Start Caddy with a Caddyfile
caddy run --config /etc/caddy/Caddyfile

# Start in the background (daemon mode)
caddy start --config /etc/caddy/Caddyfile

# Stop a background Caddy instance
caddy stop

# Start with environment file
caddy run --config /etc/caddy/Caddyfile --envfile /etc/caddy/.env

Build Caddy with plugins (xcaddy)

# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build with DNS provider plugin
xcaddy build --with github.com/caddy-dns/cloudflare

# Build with multiple plugins
xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/caddy-dns/route53 \
    --with github.com/greenpau/caddy-security

DNS challenge providers, storage backends, and other extensions are compiled into the Caddy binary. Use xcaddy to build a custom Caddy with the plugins you need.

Testing and Validation

Verify TLS configuration

Test TLS settings with common tools:

# Check TLS certificate details
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 | openssl x509 -noout -text

# Verify TLS 1.3 support
openssl s_client -connect example.com:443 -tls1_3 < /dev/null

# Check supported cipher suites
nmap --script ssl-enum-ciphers -p 443 example.com

# Quick certificate check with curl
curl -vI https://example.com 2>&1 | grep -E "subject|expire|issuer"

View Caddy logs for TLS issues

Debug certificate issuance and TLS handshake problems:

# Enable debug logging globally
{
    debug
}

# Enable debug logging per site
example.com {
    log {
        output file /var/log/caddy/debug.log
        level DEBUG
    }
    reverse_proxy localhost:8080
}

# View systemd logs for Caddy
journalctl -u caddy --no-pager -f

SSL Labs and external tests

Use external services to validate your TLS configuration:

# SSL Labs: https://www.ssllabs.com/ssltest/
# Target: A+ rating (achievable with Caddy's defaults + HSTS)

# testssl.sh for local testing
git clone --depth 1 https://github.com/drwetter/testssl.sh.git
./testssl.sh/testssl.sh https://example.com

# Security Headers: https://securityheaders.com/

Important Notes

Automatic HTTPS Requirements:

For automatic HTTPS to work, your domain must resolve to your server, and ports 80 and 443 must be accessible. If behind a firewall or NAT, use the DNS challenge instead.

Rate Limits:

Let's Encrypt has rate limits (50 certificates per registered domain per week). Use the staging CA during development and testing to avoid hitting limits.

Wildcard Certificates:

Wildcard certificates always require the DNS challenge. You must build Caddy with a DNS provider plugin using xcaddy. HTTP and TLS-ALPN challenges cannot validate wildcard domains.

On-Demand TLS Security:

Always configure the ask endpoint when using on-demand TLS to prevent abuse. Without it, anyone could trigger certificate issuance for arbitrary domains pointing to your server.

Caddy v2 vs v1:

Caddy v2 has a completely different configuration format from v1. All examples on this page use Caddy v2 syntax. If upgrading from v1, consult the official migration guide.

File Permissions:

Caddy's data directory (where certificates and keys are stored) should be accessible only to the Caddy process. Default permissions are set correctly when using the official packages.

Certificate Renewal:

Caddy automatically renews certificates before they expire (at about 2/3 of their lifetime). No cron jobs or manual intervention needed. Renewals are attempted well in advance and retried on failure.

Documentation:

Official Caddy documentation: caddyserver.com/docs. Caddyfile reference: caddyserver.com/docs/caddyfile. Community wiki and forums: caddy.community.

See Also