# How Does Caddy's Certificate Automation Work? A Source Code Deep Dive

> Explore Caddy's certificate automation source code. Learn how AutomationConfig, AutomationPolicies, and modular Issuers provision and renew TLS certificates automatically with CertMagic.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: deep-dive
- Published: 2026-03-03

---

**Caddy's certificate automation orchestrates the entire TLS certificate lifecycle through an `AutomationConfig` that provisions `AutomationPolicies`, which delegate issuance to modular `Issuer` implementations (ACME or Internal), all integrated with CertMagic for automatic renewal and OCSP stapling.**

Caddy's certificate automation eliminates manual TLS certificate management by automatically obtaining, renewing, and loading certificates for your domains. According to the caddyserver/caddy source code, this system is implemented through a sophisticated policy-based architecture in the `modules/caddytls` package that separates global orchestration settings from per-domain issuance rules.

## The Three Pillars of Certificate Automation

Caddy's certificate automation rests on three core abstractions defined in [`modules/caddytls/automation.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/automation.go).

**AutomationConfig** (lines 36-66) contains global settings that enable and tune automation, including the `RenewCheckInterval` (default 10 minutes), `OCSPCheckInterval` (default 1 hour), and `OnDemand` configuration.

**AutomationPolicy** (lines 84-154) defines per-subject rules that determine which issuer to use, how to handle ACME challenges, and when to renew certificates. Each policy can specify its own storage backend, issuer list, and on-demand permission callbacks.

**Issuer modules** are concrete implementations that obtain certificates from specific sources. The two built-in types are **ACMEIssuer** ([`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go), lines 44-88) for public certificates via Let's Encrypt or ZeroSSL, and **InternalIssuer** ([`modules/caddytls/internalissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/internalissuer.go), lines 37-55) for private PKI using local CAs.

## How the TLS App Provisions the Automation Stack

When Caddy starts, the TLS application's `Provision` method in [`modules/caddytls/tls.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/tls.go) (lines 60-78) creates a **certificate cache** (`certmagic.Cache`) and initializes the automation stack:

```go
if t.Automation != nil {
    cacheOpts.OCSPCheckInterval = time.Duration(t.Automation.OCSPCheckInterval)
    cacheOpts.RenewCheckInterval = time.Duration(t.Automation.RenewCheckInterval)
}

```

If no `automation` block is present in the configuration, Caddy automatically creates a **default public policy** (`defaultPublicAutomationPolicy`) and, for subjects that do not qualify for public certificates, a **default internal policy** (`defaultInternalAutomationPolicy`). Both are provisioned via `AutomationPolicy.Provision` (lines 92-106):

```go
t.Automation.defaultPublicAutomationPolicy = new(AutomationPolicy)
err = t.Automation.defaultPublicAutomationPolicy.Provision(t)

```

## Inside AutomationPolicy.Provision

The `AutomationPolicy.Provision` method performs four critical initialization steps. First, it performs **placeholder replacement** on subject names using Caddy's replacer to expand `${env}` variables. Second, it loads a per-policy **storage module** if configured via the `storage` field. Third, it initializes issuers—if the user supplied an `issuers` list, each is loaded via `LoadModule`; if omitted, `DefaultIssuersProvisioned` supplies a standard `ACMEIssuer`.

Fourth, for **On-Demand TLS**, when `on_demand` is true or managers are defined, it builds a `certmagic.OnDemandConfig` with a permission decision callback (lines 66-88):

```go
if ap.OnDemand || len(ap.Managers) > 0 {
    ond = &certmagic.OnDemandConfig{
        DecisionFunc: func(ctx context.Context, name string) error { … },
        Managers: ap.Managers,
    }
}

```

Finally, the method instantiates a **certmagic.Config** with all resolved options (OCSP settings, key type, preferred chains) and stores it in `ap.magic`. Issuers receive this config via the `ConfigSetter` interface (lines 56-63):

```go
for _, issuer := range ap.magic.Issuers {
    if annoying, ok := issuer.(ConfigSetter); ok {
        annoying.SetConfig(ap.magic)
    }
}

```

## Public Certificate Issuance with ACMEIssuer

The `ACMEIssuer` struct in [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go) implements `certmagic.Issuer`, `certmagic.PreChecker`, and `certmagic.Renewer`. Its `Provision` method constructs an **acmez**-compatible template via `makeIssuerTemplate` (lines 28-46):

```go
iss.template, err = iss.makeIssuerTemplate(ctx)

```

This template includes the CA directory URL (`CA` or `TestCA`), contact email for registration, challenge configuration (HTTP-01, TLS-ALPN-01, or DNS-01), and optional network proxy settings. When the DNS challenge is used, `ACMEIssuer.Provision` creates a `certmagic.DNS01Solver` from the configured DNS provider module (lines 73-84).

During a TLS handshake, the issuer's `PreCheck` method runs first to verify domain permission (checking on-demand authorization and wildcard validity). If allowed, the `Issue` method forwards the Certificate Signing Request (CSR) to the ACME server.

## Private Certificates with InternalIssuer

For domains that do not qualify for public certificates (such as `*.local` or `10.*` addresses), Caddy uses `InternalIssuer` defined in [`modules/caddytls/internalissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/internalissuer.go) (lines 37-55):

```go
type InternalIssuer struct {
    CA string `json:"ca,omitempty"`
    Lifetime caddy.Duration `json:"lifetime,omitempty"`
    SignWithRoot bool `json:"sign_with_root,omitempty"`
    ca *caddypki.CA
}

```

During provisioning, it retrieves the Certificate Authority from the PKI app (`ctx.App("pki")`) and uses Smallstep's `Authority` to sign CSRs locally. This provides valid TLS for internal networks without requiring public internet access or external validation.

## Matching Requests to Policies

When Caddy needs a certificate for a hostname, it calls `TLS.getAutomationPolicyForName` in [`modules/caddytls/tls.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/tls.go) (lines 70-88). This method iterates through `AutomationConfig.Policies` and returns the **first** policy whose subject list matches the requested name using `certmagic.MatchWildcard` for wildcard support:

```go
for _, ap := range t.Automation.Policies {
    if len(ap.subjects) == 0 { return ap }
    for _, h := range ap.subjects {
        if certmagic.MatchWildcard(name, h) { return ap }
    }
}

```

If no custom policy matches, Caddy falls back to `defaultPublicAutomationPolicy` for publicly routable domains or `defaultInternalAutomationPolicy` for private names.

## Batching and Managing Subjects

The `TLS.Manage` method groups incoming subjects by their matching automation policy to optimize resource usage. It creates a `policyToNames` map, then calls `ManageAsync` once per group rather than once per domain (lines 61-85):

```go
policyToNames := make(map[*AutomationPolicy][]string)
for subj := range subjects {
    ap := t.getAutomationPolicyForName(subj)
    policyToNames[ap] = append(policyToNames[ap], subj)
}
for ap, names := range policyToNames {
    err := ap.magic.ManageAsync(t.ctx.Context, names)
    …
}

```

This approach minimizes the number of `certmagic.Config` instances and enables efficient batch renewal operations.

## On-Demand TLS: Deferred Issuance

When a policy has `on_demand: true`, Caddy defers certificate issuance until the actual TLS handshake occurs. The permission decision is performed via the **permission module** configured under `automation.on_demand.permission`. If no module is configured and the policy is unbounded (wildcard), Caddy aborts with an error to prevent abuse.

## Automatic Renewal and OCSP Stapling

The global `AutomationConfig.RenewCheckInterval` (default 10 minutes) drives CertMagic's background renewal scanner, which checks for expiring certificates and initiates re-issuance automatically. The `OCSPCheckInterval` (default 1 hour) triggers updates to OCSP staples. Individual policies may override these intervals through their own configuration fields, allowing fine-grained control over check frequency for different certificate sets.

## Practical Configuration Examples

### Basic Automatic HTTPS with Global Settings

```caddy
{
    automation {
        renew_interval 5m
        ocsp_interval 30m
    }
}

example.com, www.example.com {
    reverse_proxy localhost:8080
}

```

This configuration relies on the `defaultPublicAutomationPolicy` to automatically obtain certificates from Let's Encrypt for the specified domains.

### Explicit Subject Management with the Automate Loader

```caddy
{
    tls {
        certificates {
            automate example.com www.example.com api.example.com
        }
    }
}

```

The `automate` loader explicitly lists managed domains, storing them in `t.automateNames` for processing by `TLS.Manage`.

### On-Demand TLS with Permission Endpoint

```caddy
{
    automation {
        on_demand {
            ask https://my-auth.example.com/allow
        }
    }
}

```

Caddy calls the `ask` endpoint during each handshake. A `200 OK` response permits certificate issuance; any other status aborts the connection.

### Wildcard Certificates with DNS Challenge

```caddy
tls {
    automation {
        policies {
            {
                subjects *.example.com
                issuers acme {
                    dns cloudflare {
                        api_token ${CLOUDFLARE_TOKEN}
                    }
                }
            }
        }
    }
}

```

The `dns cloudflare` block triggers `ACMEIssuer.Provision` to configure a `certmagic.DNS01Solver` for ACME DNS-01 challenges.

### Internal PKI for Private Domains

```caddy
{
    pki {
        ca local {
            key_type rsa2048
            lifetime 8760h
        }
    }
}

tls {
    automation {
        policies {
            {
                subjects internal.example.com *.local
                issuers internal {}
            }
        }
    }
}

```

This uses `InternalIssuer` to sign certificates with the local CA defined in the PKI app, providing valid TLS for non-public domains without external ACME servers.

## Summary

- **Caddy's certificate automation** is governed by `AutomationConfig` and `AutomationPolicy` structs defined in [`modules/caddytls/automation.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/automation.go), separating global settings from per-domain rules.
- The **TLS app** ([`modules/caddytls/tls.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/tls.go)) initializes a `certmagic.Cache` during `Provision`, then matches domains to policies via `getAutomationPolicyForName`.
- **ACMEIssuer** ([`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go)) handles public certificates using the acmez library with support for HTTP, TLS-ALPN, and DNS challenges.
- **InternalIssuer** ([`modules/caddytls/internalissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/internalissuer.go)) provides private PKI integration using the `caddypki` app for internal domains.
- **On-Demand TLS** defers issuance until handshake time, requiring explicit permission modules to prevent abuse.
- **Batch management** via `TLS.Manage` groups domains by policy and calls `ManageAsync` for efficient renewal and OCSP updates.

## Frequently Asked Questions

### What happens if I don't specify any automation configuration?

Caddy automatically creates `defaultPublicAutomationPolicy` and `defaultInternalAutomationPolicy` during the TLS app's `Provision` phase. Any publicly qualified domain uses the default ACME issuer (typically Let's Encrypt), while private domains fall back to the internal issuer, requiring no manual configuration for standard use cases.

### How does Caddy decide which automation policy to use for a domain?

Caddy calls `getAutomationPolicyForName` in [`modules/caddytls/tls.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/tls.go), which iterates through configured policies in order and returns the first match based on exact or wildcard (`certmagic.MatchWildcard`) subject matching. If no custom policy matches, it returns the default public policy for internet-facing domains or the default internal policy for private names.

### What is the difference between ACMEIssuer and InternalIssuer?

**ACMEIssuer** ([`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go)) communicates with external ACME servers like Let's Encrypt to obtain publicly trusted certificates, supporting HTTP-01, TLS-ALPN-01, and DNS-01 challenges. **InternalIssuer** ([`modules/caddytls/internalissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/internalissuer.go)) uses the local PKI app (`caddypki`) to sign certificates with a private CA, suitable for internal networks, development environments, or domains that cannot pass public validation.

### How does On-Demand TLS prevent abuse?

When `on_demand` is enabled without explicit subject lists, Caddy requires a **permission module** (configured via `ask` or custom modules) that acts as a decision callback during the TLS handshake. If the permission endpoint does not return `200 OK`, Caddy aborts the handshake before attempting issuance, preventing unauthorized certificate generation for arbitrary domains.