Self-Signed & Internal CA #
Not every deployment environment has direct access to the global internet or a public domain name verifiable by Let’s Encrypt or ZeroSSL. Local development servers (localhost), internal testing environments (staging), corporate intranets, and servers on private networks still need HTTPS data transmission security, but they can’t complete public ACME challenges. To address this need, Caddy provides a built-in solution: an Internal Certificate Authority (CA) capable of issuing and managing TLS certificates automatically for local domains without any internet dependency.
Besides using Caddy’s built-in internal CA, you can also configure Caddy to use manual certificates you provide yourself (either created with OpenSSL or issued by a third-party commercial CA). Understanding the architectural differences between using Caddy’s internal CA, manual certificates, and integration with corporate PKI systems (like HashiCorp Vault) will equip you to design the right TLS security architecture for various non-public scenarios.
Two Approaches to HTTPS Without Public ACME #
When you can’t or don’t want to use a public CA, there are two main approaches supported by Caddy:
Approach 1: Using Caddy's Built-in Internal CA (Highly Recommended)
Caddy automatically acts as a local Certificate Authority.
- Creates a standalone Root CA (Root Certificate & Private Key).
- Creates an Intermediate CA signed by the Root CA.
- Issues and renews short-lived certificates for local domains automatically.
Advantage: The certificate lifecycle is fully managed by Caddy; browsers won't show
a warning after you install the Root CA once into the system.
Approach 2: Using Manual Certificates (Custom Certificates)
You provide the certificate file (.crt) and private key (.key) yourself to Caddy.
- The private key and certificate are created using external tools (like OpenSSL).
- Caddy only acts as the user of those certificates for TLS negotiation.
Advantage: Very flexible; can be used for raw IP addresses or internal domains
whose certificates were purchased from a specific commercial CA.
Caddy’s Internal PKI Architecture #
When the internal CA feature is enabled, Caddy automatically sets up a mini Public Key Infrastructure (PKI) inside your server. Understanding the lifetime parameters of Caddy’s PKI components is important for maintaining system reliability:
flowchart TD
Root["Caddy Root CA<br>(Valid 10 Years / 3650 Days)"] -->|"Signs"| Intermediate["Caddy Intermediate CA<br>(Valid 30 Days, auto-renewed every 7 days)"]
Intermediate -->|"Signs"| Leaf["Leaf Certificate (Valid 7 Days)<br>(Auto-renewed before expiry)"]
style Root stroke:#0288d1,stroke-width:2px
style Leaf stroke:#43a047,stroke-width:2pxThis internal mechanism is designed to be very secure:
- Short-Lived Leaf Certificates: Since the final certificate only lasts 7 days, the risk of certificate misuse if a domain private key leaks is minimal. Caddy renews it in the background without users noticing.
- Automatic Intermediate CA Renewal: The Intermediate Key is proactively renewed by Caddy every week before its 30-day validity expires, using the Root Key safely stored on the server disk.
How to Enable Caddy’s Internal CA #
There are three common ways to tell Caddy to use its built-in internal CA, depending on the scope of domains you serve:
1. Using the ’localhost’ Keyword (Automatic) #
Caddy intelligently assumes that the localhost hostname or the local IP 127.0.0.1 is a local environment and immediately enables the internal CA without extra configuration:
# Using localhost - automatically triggers the internal CA
localhost {
reverse_proxy localhost:3000
}
2. Using the Global ’local_certs’ Option #
If you have many site blocks with local domain names (like .local or .internal) and want them all to use the internal CA, you can set it globally:
{
# Force all sites to use the internal CA
local_certs
}
app.internal {
reverse_proxy localhost:3000
}
api.local {
file_server
}
3. Using the Per-Site ’tls internal’ Directive #
You can also specify internal CA usage for specific site blocks only using the tls internal directive:
# Only this site uses the internal CA
dev.example.com {
tls internal
reverse_proxy localhost:4000
}
Installing the Root CA into Systems and Browsers #
Certificates issued by Caddy’s internal CA won’t be immediately trusted by your operating system or web browser because Caddy isn’t a globally registered public CA. You must install Caddy’s Root CA into your system’s trust store.
1. Using the Automatic ‘caddy trust’ Command #
If Caddy runs on your local machine (a development computer), you can use Caddy’s built-in command to automatically install the Root CA into your OS and browser:
# Install Caddy's Root CA into the system trust store (Requires sudo/admin access)
caddy trust
# Remove Caddy's Root CA if no longer used
caddy untrust
2. Caddy Root CA File Locations #
If Caddy runs on another server on your local network, you must copy the Root CA file from that server and install it manually on your client computers. The Root CA file location (root.crt) depends on the Caddy installation:
- Linux (Systemd):
/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt - macOS:
~/Library/Application Support/Caddy/pki/authorities/local/root.crt - Windows:
%APPDATA%\Caddy\pki\authorities\local\root.crt
3. Manual Installation Guides for Various Operating Systems #
On macOS (Keychain Access) #
# Install and trust root.crt via the macOS terminal
security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain-db root.crt
On Linux (Ubuntu/Debian) #
# Copy the file to the system certificates directory
sudo cp root.crt /usr/local/share/ca-certificates/caddy-root.crt
# Update the system trusted certificate database
sudo update-ca-certificates
On Windows (PowerShell/CMD) #
# Run PowerShell as Administrator, then run:
certutil -addstore -f "ROOT" root.crt
Internal CA Configuration in Docker Compose #
When running Caddy inside a Docker container for local development, you need to mount Caddy’s data folder so the Root CA file can be accessed from the host computer.
version: "3.7"
services:
caddy:
image: caddy:2.8.4
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
# Mount the PKI folder so we can access it to trust on the host
- ./caddy-pki:/data/caddy/pki
volumes:
caddy_data:
caddy_config:
After the container first runs, the ./caddy-pki/ folder fills with certificates. Just take the ./caddy-pki/authorities/local/root.crt file and install it on your host computer’s OS using the manual installation methods described earlier.
Caddy as a Local ACME Server #
A remarkable Caddy feature that’s rarely well documented is its ability to act as a local ACME server. This is very useful in enterprise environments: one main Caddy instance acts as an internal CA hub serving certificate requests from many other web server instances inside your internal network via the standard ACME protocol.
flowchart TD
subgraph Corporate Private Network
A["Client Web Server (Caddy 2)"]
B["Client Backend Service (Nginx/Certbot)"]
C["Main Caddy Instance (Master CA)"]
end
A -->|1. ACME /newOrder Request| C
B -->|2. ACME /newOrder Request| C
C -->|3. Send Local Challenge| A
C -->|4. Send Local Challenge| B
A -->|5. Complete Challenge| C
B -->|6. Complete Challenge| C
C -->|7. Issue Internal Certificate| A
C -->|8. Issue Internal Certificate| BConfiguring the Caddy Master CA (ACME Service Provider) #
On the main Caddy that will act as the CA, you must define a local PKI authority and enable the internal ACME endpoint in the Caddyfile:
# Caddyfile on the Master CA server (IP: 192.168.1.10)
{
# Set the local PKI authority name
pki {
ca local {
name "Head Office CA"
}
}
}
# Provide an ACME endpoint for other internal machines
# We listen on the special acme.internal domain
acme.internal {
# Enable Caddy's built-in ACME server
acme_server
# Still use the internal CA to secure acme.internal's own communication
tls internal
}
Configuring the Client Caddy (Certificate Requester) #
On client web servers in your internal network, configure their Caddyfiles to point certificate issuance at your Master CA server:
# Caddyfile on the Client Server
{
cert_issuer acme {
# Point the CA at our Master CA server
ca https://acme.internal/acme/local/directory
# Tell the client Caddy to trust the Master's Root CA certificate
# so the initial ACME communication doesn't fail with an SSL warning
trusted_roots /etc/ssl/certs/master-ca-root.crt
}
}
# This internal domain will get its certificate from the Master CA
service.internal {
reverse_proxy localhost:8080
}
Manual Certificate Configuration #
If you already have your own certificate and private key files created outside Caddy, you can write them directly into your site block using the tls directive:
# Using existing external certificates
example.internal {
# Format: tls [certificate_file] [private_key_file]
tls /etc/ssl/certs/example.internal.crt /etc/ssl/private/example.internal.key
reverse_proxy localhost:5000
}
File Permission Rules #
Caddy must be able to read the certificate and private key files. Failing to read these files is one of the reasons Caddy can’t start during a restart.
- Linux (User ‘caddy’): If Caddy runs as a systemd service, it uses the
caddyaccount. Give file ownership to that user:
# Set certificate and private key file ownership to caddy
sudo chown -R caddy:caddy /etc/ssl/private/
# Set strict access permissions (only caddy may read the private key)
sudo chmod 600 /etc/ssl/private/example.internal.key
sudo chmod 644 /etc/ssl/certs/example.internal.crt
Creating Manual Certificates with OpenSSL (Correct SAN) #
Many developers run into problems using OpenSSL-created certificates because modern browsers (like newer Chrome versions) reject them with the ERR_CERT_COMMON_NAME_INVALID error. This happens because modern browsers require the Subject Alternative Name (SAN) extension; listing the domain only in the Common Name (CN) is no longer considered sufficient.
Here’s a guide to creating a proper OpenSSL config file (openssl.cnf) that includes SAN:
# Save this file as openssl.cnf
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
C = ID
O = Our Company
CN = server.internal
[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = server.internal
DNS.2 = *.server.internal
IP.1 = 192.168.1.100
IP.2 = 127.0.0.1
Run the following OpenSSL commands to create a self-signed certificate valid for 10 years using the config above:
# Create a private key and SAN-enabled certificate
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout server.internal.key \
-out server.internal.crt \
-config openssl.cnf -extensions v3_req
The resulting server.internal.crt and server.internal.key files can now be installed in the Caddyfile and will be trusted by browsers after you install the .crt file into the client computer’s trust store.
Internal CA Troubleshooting #
1. The Browser Shows an SSL Warning After ‘caddy trust’ #
- Cause: Some browsers (like Mozilla Firefox) don’t use the OS built-in trust store. Firefox uses its own internal certificate database.
- Solution: Open Firefox → Settings → Privacy & Security → View Certificates → Authorities → click Import → select Caddy’s
root.crtfile, then check the “Trust this CA to identify websites” option.
2. The ‘curl’ Tool Rejects Caddy’s Internal Certificate #
- Symptom: You can access the site in a browser without warnings, but the
curl https://localhostcommand replies withSSL certificate problem: unable to get local issuer certificate. - Cause:
curluses a different OS trust store or doesn’t pick up trust store changes until the terminal is restarted. - Solution: Run your system’s certificate database update (
sudo update-ca-certificateson Linux), restart your terminal session, or use the--cacertparameter on the curl command to explicitly point at Caddy’s root CA file:
# Make a curl query pointing at Caddy's Root CA manually
curl --cacert ~/.local/share/caddy/pki/authorities/local/root.crt https://localhost/api/health
Summary #
- Internal CA — Caddy’s built-in feature for issuing automatic TLS certificates on local domains without needing an internet connection or public domain name.
- Secure Lifecycle — Issued leaf certificates only last 7 days, periodically renewed by Caddy using an intermediate CA key.
- Simple Activation — Caddy automatically enables the internal CA for the
localhostkeyword or via per-sitetls internalconfiguration in the Caddyfile.- Root CA Installation — To get certificates trusted by browsers, run
caddy truston localhost, or copy Caddy’sroot.crtfile for manual import into other browsers/clients.- Local ACME Server — Caddy can act as an internal ACME service provider (
acme_server) to serve encrypted certificate requests from other internal servers.- OpenSSL Certificates — When creating manual certificates with OpenSSL, you must use the Subject Alternative Name (SAN) extension so modern browsers don’t reject them.