Skip to main content
Certificate issuers are modules that obtain and manage TLS certificates. Caddy supports multiple issuer types for different use cases.

ACME Issuer

The ACME issuer manages certificates using the ACME protocol (RFC 8555), compatible with Let’s Encrypt, ZeroSSL ACME, and other ACME CAs.

Configuration

module
string
default:"acme"
Module identifier for ACME issuer.
ca
string
default:"https://acme-v02.api.letsencrypt.org/directory"
The URL to the CA’s ACME directory endpoint.Common values:
  • https://acme-v02.api.letsencrypt.org/directory (Let’s Encrypt production)
  • https://acme-staging-v02.api.letsencrypt.org/directory (Let’s Encrypt staging)
  • https://acme.zerossl.com/v2/DV90 (ZeroSSL ACME)
test_ca
string
The URL to the test CA’s ACME directory endpoint.This endpoint is only used during retries if there is a failure using the primary CA.
email
string
Your email address, so the CA can contact you if necessary.
Not required, but strongly recommended so you can be reached if there is a problem. Your email is not sent to any Caddy mothership or used for any purpose other than ACME transactions.
profile
string
Optionally select an ACME profile to use for certificate orders.Must be a profile name offered by the ACME server, which are listed at its directory endpoint.
account_key
string
If you have an existing account with the ACME server, put the private key here in PEM format.The ACME client will look up your account information with this key first before trying to create a new one. Supports placeholders (e.g., environment variables).
external_account
object
If using an ACME CA that requires external account binding, specify the CA-provided credentials here.
{
  "key_id": "your-eab-key-id",
  "mac_key": "your-eab-hmac-key"
}
acme_timeout
duration
default:"0"
Time to wait before timing out an ACME operation.Default of 0 means no timeout.
certificate_lifetime
duration
default:"0"
The validity period to ask the CA to issue a certificate for.
Not all CAs support this. Check with your CA’s ACME documentation to see if this is allowed and what values may be used.
Default of 0 means CA chooses lifetime. This value computes the “notAfter” field of the ACME order; the system must have a reasonably synchronized clock.
EXPERIMENTAL: Subject to change.

ACME Challenges

challenges
object
Configures the various ACME challenge types.

HTTP Challenge

challenges.http.disabled
boolean
default:"false"
If true, the HTTP challenge will be disabled.
challenges.http.alternate_port
integer
An alternate port on which to service the HTTP challenge.
The HTTP challenge port is hard-coded into the ACME spec (port 80) and cannot be changed. You would need to forward packets from port 80 to this alternate port.

TLS-ALPN Challenge

challenges.tls_alpn.disabled
boolean
default:"false"
If true, the TLS-ALPN challenge will be disabled.
challenges.tls_alpn.alternate_port
integer
An alternate port on which to service the TLS-ALPN challenge.
The TLS-ALPN challenge port is hard-coded into the spec (port 443) and cannot be changed. You would need to forward packets from port 443 to this alternate port.

DNS Challenge

challenges.dns
object
Configures the ACME DNS challenge.
This is the only challenge type that does not require a direct connection to Caddy from an external server. It’s typically used when behind firewalls or for wildcard certificates.
challenges.dns.provider
object
required
The DNS provider module to use which will manage DNS records relevant to the ACME challenge.Example providers: cloudflare, route53, gandi, etc.
challenges.dns.ttl
duration
The TTL of the TXT record used for the DNS challenge.
challenges.dns.propagation_delay
duration
default:"0"
How long to wait before starting propagation checks.
challenges.dns.propagation_timeout
duration
default:"2m"
Maximum time to wait for temporary DNS record to appear.Set to -1 to disable propagation checks.
challenges.dns.resolvers
array
Custom DNS resolvers to prefer over system/built-in defaults.Often necessary when using split-horizon DNS.Example: ["8.8.8.8:53", "1.1.1.1:53"]
challenges.dns.override_domain
string
Override the domain to use for the DNS challenge.This delegates the challenge to a different domain (e.g., one that updates faster or has a provider API).

Challenge Distribution

challenges.bind_host
string
Optionally customize the host to which a listener is bound if required for solving a challenge.
challenges.distributed
boolean
default:"true"
Whether distributed solving is enabled.
Applies to HTTP and TLS-ALPN challenges. When enabled, challenge info is written to storage backend for distribution across a cluster.
Set to false only if you cannot reliably use storage backend for writing/distributing challenge info. When disabled, challenges can only be solved from the Caddy instance that initiated the challenge (with exception for HTTP challenges initiated with the same ACME account).

Certificate Chain Preferences

preferred_chains
object
Preferences for selecting alternate certificate chains, if offered by the CA.By default, the first offered chain will be selected.
preferred_chains.smallest
boolean
Prefer chains with the fewest number of bytes.
preferred_chains.root_common_name
array
Select first chain having a root with one of these common names.Example: ["ISRG Root X1", "ISRG Root X2"]
preferred_chains.any_common_name
array
Select first chain that has any issuer with one of these common names.

Advanced Options

trusted_roots_pem_files
array
An array of files of CA certificates to accept when connecting to the ACME CA.
Generally, you should only use this if the ACME CA endpoint is internal or for development/testing purposes.
network_proxy
object
Forward proxy module configuration for ACME requests.

ACME Examples

Basic Let’s Encrypt

{
  "module": "acme",
  "email": "[email protected]"
}

DNS Challenge with Cloudflare

{
  "module": "acme",
  "email": "[email protected]",
  "challenges": {
    "dns": {
      "provider": {
        "name": "cloudflare",
        "api_token": "{env.CLOUDFLARE_API_TOKEN}"
      }
    }
  }
}

External Account Binding

{
  "module": "acme",
  "ca": "https://acme.sectigo.com/v2/InCommonRSAOV",
  "email": "[email protected]",
  "external_account": {
    "key_id": "your-eab-kid",
    "mac_key": "your-eab-hmac-key"
  }
}

Preferred Chain Selection

{
  "module": "acme",
  "email": "[email protected]",
  "preferred_chains": {
    "root_common_name": ["ISRG Root X1"]
  }
}

ZeroSSL Issuer

The ZeroSSL issuer uses the ZeroSSL API directly (distinct from ZeroSSL’s ACME endpoint).
To use ZeroSSL’s ACME endpoint, use the ACME issuer configured with ZeroSSL’s ACME directory.

Configuration

module
string
default:"zerossl"
Module identifier for ZeroSSL issuer.
api_key
string
required
The API key (or “access key”) for using the ZeroSSL API.Supports placeholders for environment variables.
validity_days
integer
How many days the certificate should be valid for.
Only certain values are accepted; see ZeroSSL documentation.
listen_host
string
The host to bind to when opening a listener for verifying domain names (or IPs).
alternate_http_port
integer
If HTTP is forwarded from port 80, specify the forwarded port here.
cname_validation
object
Use CNAME validation instead of HTTP.ZeroSSL’s API uses CNAME records for DNS validation, similar to how Let’s Encrypt uses TXT records for the DNS challenge.Accepts same configuration as ACME DNS challenge (provider, TTL, propagation settings, etc.).

ZeroSSL Example

{
  "module": "zerossl",
  "api_key": "{env.ZEROSSL_API_KEY}",
  "validity_days": 90
}

Internal Issuer

The internal issuer generates certificates using a locally-configured CA (managed by Caddy’s PKI app).
Perfect for development, testing, or internal services. Certificates are signed by Caddy’s internal CA.

Configuration

module
string
default:"internal"
Module identifier for internal issuer.
ca
string
default:"local"
The ID of the CA to use for signing.The CA can be configured with the pki app.
lifetime
duration
default:"12h"
The validity period of certificates.
sign_with_root
boolean
default:"false"
If true, the root will be the issuer instead of the intermediate.
NOT recommended. Should only be used when devices/clients do not properly validate certificate chains.

Internal Issuer Example

{
  "module": "internal",
  "ca": "local",
  "lifetime": "24h"
}

Development Configuration

{
  "apps": {
    "tls": {
      "automation": {
        "policies": [
          {
            "subjects": ["localhost", "*.local", "127.0.0.1"],
            "issuers": [
              {
                "module": "internal",
                "ca": "local"
              }
            ]
          }
        ]
      }
    }
  }
}

Caddyfile Configuration

ACME Issuer

example.com {
  tls {
    issuer acme {
      dir https://acme-v02.api.letsencrypt.org/directory
      email [email protected]
      dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
  }
}

ZeroSSL Issuer

example.com {
  tls {
    issuer zerossl {env.ZEROSSL_API_KEY} {
      validity_days 90
    }
  }
}

Internal Issuer

localhost {
  tls {
    issuer internal {
      ca local
      lifetime 24h
    }
  }
}

Multi-Issuer Redundancy

Caddy can use multiple issuers for redundancy. If the first fails, it tries the next:
{
  "issuers": [
    {
      "module": "acme",
      "ca": "https://acme-v02.api.letsencrypt.org/directory"
    },
    {
      "module": "acme",
      "ca": "https://acme.zerossl.com/v2/DV90"
    }
  ]
}

Best Practices

Rate Limits: Let’s Encrypt has rate limits. Use the staging environment for testing to avoid hitting production limits.
DNS Challenge: Required for wildcard certificates and useful when Caddy is behind a firewall. Requires a supported DNS provider.
Internal CA: Great for development, but clients will show security warnings unless they trust your CA. Install Caddy’s root certificate on client devices for testing.

Build docs developers (and LLMs) love