LFCE 55 ๐ง Caddy โ Automatic HTTPS
Caddy’s automatic HTTPS is the feature that distinguishes it from every other web server in this series. Nginx and Apache require the administrator to obtain certificates, write the TLS directives, and set up a renewal mechanism. Caddy performs all of these steps without explicit configuration: a site block containing a public hostname is enough to trigger certificate acquisition, HTTPS serving, HTTP-to-HTTPS redirection, and renewal before expiry . The previous chapter covered the basic Caddyfile and the reverse_proxy directive; this chapter covers the automatic HTTPS behavior itself: the certificate authorities, the ACME challenges, the internal CA for local development, the wildcard certificate requirement, and the on-demand TLS mechanism for custom domains.
Key point: Automatic HTTPS is triggered by the site address. A public hostname like example.com uses Let’s Encrypt or ZeroSSL and requires ports 80 and 443 to be reachable from the internet. A local address like localhost or an internal IP uses Caddy’s internal CA. The tls internal directive forces the internal CA for a site that would otherwise use a public one . Wildcard certificates (*.example.com) require the DNS challenge, which in turn requires a DNS provider plugin.
Why automatic HTTPS is a design decision, not a feature toggle
The multi-issuer fallback problem.
Caddy does not rely on a single certificate authority. It attempts Let’s Encrypt first, then ZeroSSL. If both fail, it backs off exponentially, with a maximum interval of one day between attempts, for up to 30 days. During retries with Let’s Encrypt, it switches to the staging environment to avoid rate limits . This fallback behavior is built into the server, not configured by the administrator. The result is that a transient failure at one CA does not take the site down permanently.
The challenge-selection problem.
ACME requires the server to prove control of the domain. Caddy supports three challenge types: HTTP-01, which serves a token on port 80; TLS-ALPN-01, which uses a special TLS handshake on port 443; and DNS-01, which creates a TXT record and requires a DNS provider plugin . The HTTP and TLS-ALPN challenges are enabled by default, and Caddy randomly chooses between them to avoid accidental dependence on one, then learns which is more successful over time . The DNS challenge is required for wildcard certificates and is configured explicitly.
The internal-CA problem.
A local development site does not have a public DNS record and cannot complete an ACME challenge. Caddy’s internal CA issues certificates from a locally generated root for localhost, 127.0.0.1, and internal IP addresses. The root certificate is installed into the system trust store on first use, or manually with caddy trust if Caddy runs as an unprivileged user . This makes HTTPS possible in development without a public domain or an internet connection to a CA.
a. Public certificates and the default behavior
A site block with a public hostname triggers automatic HTTPS. No tls directive is required.
# /etc/caddy/Caddyfile
example.com {
root * /var/www/example
file_server
}
When Caddy starts, it detects the hostname in the site address, obtains a certificate from Let’s Encrypt or ZeroSSL, serves the site over HTTPS on port 443, and redirects HTTP requests on port 80 to the HTTPS URL . The redirect is part of the automatic behavior; it does not require a redir directive or a separate site block.
The ACME account email can be set globally for expiry notifications. The email is optional, but it is recommended because it allows the CA to contact the administrator if there is an issue with the account .
{
email admin@example.com
}
example.com {
root * /var/www/example
file_server
}
The global options block at the top of the Caddyfile applies to all sites. The email option registers the ACME account with the specified address. Caddy may use a different CA for renewal than it used for the initial issuance, so an expiry email from Let’s Encrypt does not necessarily mean the current certificate is from Let’s Encrypt .
b. The internal CA for local development
Sites with local addresses use Caddy’s internal CA instead of a public one. The tls internal directive forces the internal CA for a site that would otherwise qualify for a public certificate.
# Local development with the internal CA
localhost {
root * /var/www/dev
file_server
}
# Internal hostname, forced to use the internal CA
app.internal {
tls internal
reverse_proxy 127.0.0.1:3000
}
The localhost site uses the internal CA automatically because the address is not a public hostname. The app.internal site uses the .internal TLD, which is excluded from public certificates, and the tls internal directive makes the choice explicit .
The root certificate is stored in Caddy’s data directory, typically $HOME/.local/share/caddy/pki/authorities/local/root.crt. Caddy attempts to install it into the system trust store when it first issues a certificate. If it runs as an unprivileged user or in a container, the installation may fail, and the administrator must run caddy trust or copy the root certificate to the client machines . A client that does not trust the root certificate sees a warning for every site served by the internal CA.
c. Wildcards, DNS challenge, and on-demand TLS
A wildcard certificate covers every subdomain of a domain (*.example.com). Let’s Encrypt requires the DNS-01 challenge for wildcards, which means Caddy must create a TXT record in the domain’s DNS zone. This requires a DNS provider plugin, which is a separate build of Caddy that includes the plugin for the specific provider.
# Global DNS provider configuration
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
*.example.com {
root * /var/www
file_server
}
The acme_dns global option configures the DNS provider for all sites. The provider plugin must be compiled into the Caddy binary; the standard build does not include it . The wildcard site block then obtains a certificate for *.example.com using the DNS challenge. As of Caddy 2.10, a wildcard certificate can be used for individual subdomains in the configuration without obtaining separate certificates .
On-demand TLS is a different mechanism. It obtains a certificate during the first TLS handshake for a domain, rather than at startup. It is used by SaaS platforms that serve customer-owned domains, where the set of domains is not known in advance. The on_demand_tls global option must include an ask endpoint that Caddy queries to determine whether a certificate should be issued for a domain. Without the ask endpoint, the feature is an abuse vector .
{
on_demand_tls {
ask http://localhost:9123/check
}
}
https:// {
tls {
on_demand
}
reverse_proxy localhost:8080
}
The ask endpoint receives the domain name and returns HTTP 200 if the certificate should be issued. The application checks the domain against its database of customer domains. The https:// site block with the tls { on_demand } directive handles the certificate acquisition for any domain that passes the check . This is the mechanism that allows a single Caddy instance to serve an unbounded number of customer domains without configuration changes.
Complete Example Session
# ============================================
# PART 1: PUBLIC SITE WITH AUTOMATIC HTTPS
# ============================================
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
{
email admin@example.com
}
example.com {
root * /var/www/example
file_server
}
EOF
# ============================================
# PART 2: VALIDATE AND RELOAD
# ============================================
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# ============================================
# PART 3: VERIFY THE CERTIFICATE
# ============================================
curl -I https://example.com
# Output: HTTP/2 200
# The certificate is issued by Let's Encrypt or ZeroSSL
# ============================================
# PART 4: VERIFY THE HTTP REDIRECT
# ============================================
curl -I http://example.com
# Output: HTTP/1.1 308 Permanent Redirect
# Location: https://example.com/
# ============================================
# PART 5: LOCAL DEVELOPMENT WITH INTERNAL CA
# ============================================
sudo tee -a /etc/caddy/Caddyfile > /dev/null <<'EOF'
localhost {
root * /var/www/dev
file_server
}
EOF
# ============================================
# PART 6: FORCE INTERNAL CA FOR AN INTERNAL HOST
# ============================================
sudo tee -a /etc/caddy/Caddyfile > /dev/null <<'EOF'
app.internal {
tls internal
reverse_proxy 127.0.0.1:3000
}
EOF
# ============================================
# PART 7: INSTALL THE INTERNAL ROOT CERTIFICATE
# ============================================
sudo caddy trust
# Installs the root CA into the system trust store
# ============================================
# PART 8: WILDCARD CERTIFICATE WITH DNS CHALLENGE
# ============================================
# Requires a Caddy build with the DNS provider plugin
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
*.example.com {
root * /var/www
file_server
}
EOF
# ============================================
# PART 9: ON-DEMAND TLS FOR CUSTOM DOMAINS
# ============================================
sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
{
on_demand_tls {
ask http://localhost:9123/check
}
}
https:// {
tls {
on_demand
}
reverse_proxy localhost:8080
}
EOF
# ============================================
# PART 10: CHECK THE LOGS FOR CERTIFICATE ACTIVITY
# ============================================
sudo journalctl -u caddy -n 30
# Output shows certificate obtained, renewed, or failed
The ten parts demonstrate the automatic HTTPS surface: public certificate issuance, HTTP redirect verification, internal CA for local development, forced internal CA, root certificate installation, wildcard certificate configuration, and on-demand TLS. The critical distinction is between Parts 1 and 5: the public site uses Let’s Encrypt, and the local site uses the internal CA.
Quick Reference
Automatic HTTPS Triggers
| Site Address | Certificate Source | Requirements |
|---|---|---|
example.com | Let’s Encrypt / ZeroSSL | Ports 80 and 443 reachable |
localhost | Internal CA | None |
app.internal | Internal CA | tls internal |
*.example.com | Public CA | DNS challenge + plugin |
https:// (catch-all) | Public CA | on_demand_tls + ask endpoint |
ACME Challenges
| Challenge | Port | Use Case |
|---|---|---|
| HTTP-01 | 80 | Default; requires public reachability |
| TLS-ALPN-01 | 443 | Default; requires public reachability |
| DNS-01 | DNS | Wildcards; requires provider plugin |
tls Directive Options
| Option | Purpose |
|---|---|
tls internal | Use the internal CA |
tls { dns <provider> } | DNS challenge with provider |
tls { on_demand } | Obtain certificate on first handshake |
tls <email> | ACME account email |
Verification Commands
| Command | Purpose |
|---|---|
caddy validate --config /etc/caddy/Caddyfile | Validate configuration |
caddy trust | Install internal root CA |
curl -I https://example.com | Verify HTTPS response |
curl -I http://example.com | Verify HTTP redirect |
journalctl -u caddy | Check certificate activity |
Best Practices
โ Do This:
# Use a public hostname for automatic public certificates
example.com {
root * /var/www
file_server
} # โ
# Set the ACME account email globally
{
email admin@example.com
} # โ
# Use tls internal for local development
localhost {
tls internal
file_server
} # โ
# Install the internal root CA on client machines
caddy trust # โ
# Use the DNS challenge for wildcards
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
} # โ
# Use on-demand TLS with an ask endpoint for custom domains
{
on_demand_tls {
ask http://localhost:9123/check
}
} # โ
โ Don’t Do This:
# Use a public hostname without DNS pointing to the server
# The ACME challenge fails # โ
# Block port 80 at the firewall
# The HTTP challenge and the redirect both require it # โ
# Expect a wildcard certificate without a DNS plugin
# Let's Encrypt requires DNS-01 for wildcards # โ
# Use on-demand TLS without an ask endpoint
# It is an abuse vector # โ
# Forget to install the internal root CA on clients
# Every internal site shows a certificate warning # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Certificate not issued | DNS does not point to the server, or ports 80/443 blocked | Verify DNS and firewall rules |
| HTTP redirect missing | Port 80 blocked | Open port 80 |
| Internal site shows warning | Root CA not trusted on the client | caddy trust or copy the root certificate |
| Wildcard certificate fails | DNS plugin not included or provider misconfigured | Use a Caddy build with the plugin; check credentials |
| On-demand TLS abused | ask endpoint missing or permissive | Add the ask endpoint; validate domains |
| Renewal fails silently | CA unreachable or challenge failing | Check logs; Caddy backs off exponentially |
Real-World Examples
1. Public site with automatic HTTPS
example.com {
root * /var/www
file_server
}
2. Local development with internal CA
localhost {
tls internal
file_server
}
3. Internal hostname with forced internal CA
app.internal {
tls internal
reverse_proxy 127.0.0.1:3000
}
4. Wildcard with DNS challenge
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
*.example.com {
root * /var/www
file_server
}
5. On-demand TLS
{
on_demand_tls {
ask http://localhost:9123/check
}
}
https:// {
tls { on_demand }
reverse_proxy localhost:8080
}
6. Set ACME email
{
email admin@example.com
}
7. Install internal root CA
caddy trust
8. Verify HTTPS response
curl -I https://example.com
9. Verify HTTP redirect
curl -I http://example.com
10. Check certificate logs
journalctl -u caddy -n 30
Visual
Automatic HTTPS Decision Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE SITE ADDRESS DETERMINES THE CERTIFICATE SOURCE โ
โ โ
โ Site address: example.com โ
โ โ โ
โ โโโ Public hostname? โ YES โ
โ โ โโโ Let's Encrypt โ ZeroSSL โ fallback โ
โ โ โโโ HTTP-01 or TLS-ALPN-01 challenge โ
โ โ โ
โ โโโ Wildcard (*.example.com)? โ YES โ
โ โโโ DNS-01 challenge required โ
โ โโโ DNS provider plugin required โ
โ โ
โ Site address: localhost or internal IP โ
โ โ โ
โ โโโ Public hostname? โ NO โ
โ โโโ Internal CA โ
โ โโโ Root installed with caddy trust โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ACME Challenge Types
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THREE WAYS TO PROVE DOMAIN CONTROL โ
โ โ
โ HTTP-01: โ
โ Client โ Caddy: GET /.well-known/acme-challenge/<token> โ
โ Caddy serves the token โ CA verifies โ certificate issued โ
โ Requires: port 80 open โ
โ โ
โ TLS-ALPN-01: โ
โ Client โ Caddy: TLS handshake with special ALPN value โ
โ Caddy completes the handshake โ CA verifies โ issued โ
โ Requires: port 443 open โ
โ โ
โ DNS-01: โ
โ Caddy creates TXT record via DNS provider plugin โ
โ CA queries DNS โ verifies the record โ issued โ
โ Requires: DNS plugin, no open ports โ
โ Required for: wildcard certificates โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Internal CA Trust Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE ROOT CERTIFICATE MUST BE TRUSTED BY THE CLIENT โ
โ โ
โ Caddy host: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ caddy trust โ โ
โ โ โ installs root.crt into the system trust store โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Other clients: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Copy root.crt from the Caddy host: โ โ
โ โ $HOME/.local/share/caddy/pki/authorities/local/root.crtโ โ
โ โ โ install into the client's trust store โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Without trust: certificate warning on every internal site. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Trigger | Site address with a public hostname |
| Default CAs | Let’s Encrypt, then ZeroSSL |
| Fallback behavior | Exponential backoff, staging on retry |
| Default challenges | HTTP-01 and TLS-ALPN-01 |
| Wildcard challenge | DNS-01 (requires plugin) |
| Local development | Internal CA |
| Force internal CA | tls internal |
| Internal root location | $HOME/.local/share/caddy/pki/authorities/local/root.crt |
| Root installation | caddy trust |
| On-demand TLS | on_demand_tls + ask endpoint |
| HTTP redirect | Automatic when a hostname is configured |
Key takeaways:
- Automatic HTTPS is triggered by the site address. A public hostname obtains a certificate from Let’s Encrypt or ZeroSSL, serves HTTPS, and redirects HTTP. No
tlsdirective is required . - The default challenges are HTTP-01 and TLS-ALPN-01. They require ports 80 and 443 to be reachable from the internet. Caddy randomly chooses between them and learns which is more successful .
- Wildcard certificates require the DNS-01 challenge. The DNS challenge requires a DNS provider plugin, which is a separate Caddy build. The
acme_dnsglobal option or thetls { dns ... }directive configures it . - The internal CA serves local addresses.
localhost, internal IPs, and the.internalTLD use the internal CA automatically. Thetls internaldirective forces it for a site that would otherwise qualify for a public certificate . - The internal root must be trusted by clients.
caddy trustinstalls it on the Caddy host. Other clients need the root certificate copied and installed into their trust stores . - On-demand TLS is for customer-owned domains. It obtains a certificate during the first handshake. The
askendpoint is mandatory to prevent abuse, and the application must validate the domain before returning HTTP 200 .
Remember: Caddy’s automatic HTTPS is not a configuration option; it is the default behavior for any site with a public hostname. The certificate is obtained, the redirect is configured, and the renewal is scheduled without a single TLS directive. The internal CA covers local development, the DNS challenge covers wildcards, and on-demand TLS covers custom domains. The administrator’s job is to point the DNS, open the ports, and choose the certificate source when the default is not appropriate. The rest is automatic.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!