# How Caddy Automatic HTTPS Works with ZeroSSL and Let's Encrypt: A Technical Deep Dive

> Explore Caddy's automatic HTTPS process. Learn how it integrates with ZeroSSL and Let's Encrypt using CertMagic for seamless TLS certificate provisioning.

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

---

**Caddy's automatic HTTPS provisions TLS certificates for every configured hostname through a three-phase process that discovers domains, creates CertMagic automation policies, and uses the ACMEIssuer driver to handle both Let's Encrypt and ZeroSSL (with automatic EAB credential generation) through a unified code path.**

Caddy is the only web server that enables HTTPS by default, automatically obtaining and renewing certificates without manual configuration. According to the caddyserver/caddy source code, this automatic HTTPS system supports multiple certificate authorities including Let's Encrypt and ZeroSSL through a modular architecture centered around [`modules/caddyhttp/autohttps.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/autohttps.go) and the `ACMEIssuer` implementation in [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go).

## Phase 1: Hostname Discovery with automaticHTTPSPhase1

When Caddy starts, the HTTP app calls `App.automaticHTTPSPhase1` (line 71 in [`modules/caddyhttp/autohttps.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/autohttps.go)) to scan the configuration for domains requiring certificates.

1. **Provision matchers** – All routes are provisioned so host matchers (`MatchHost`) are decoded and available for inspection.
2. **Collect domain names** – For each server, the code builds `serverDomainSet` (lines 49-64), skipping names listed in `AutoHTTPS.Skip`.
3. **Determine certificate requirements** – If `AutoHTTPS.DisableCerts` is false, every qualified name (validated via `certmagic.SubjectQualifiesForCert`) that isn't in `SkipCerts` is added to `uniqueDomainsForCerts` (lines 98-106).
4. **Register ECH names** – The TLS app receives the server names for Encrypted Client Hello support via `app.tlsApp.RegisterServerNames`.

At the end of this phase, Caddy maintains two critical maps: `uniqueDomainsForCerts` (domains needing certificates) and `redirDomains` (HTTP to HTTPS redirect destinations).

## Phase 2: Creating Automation Policies for ACME Issuers

After hostname discovery completes, `App.createAutomationPolicies` (lines 41-88 in [`autohttps.go`](https://github.com/caddyserver/caddy/blob/main/autohttps.go)) constructs CertMagic automation policies governing how certificates are obtained.

**Base policy creation** – If the user doesn't supply custom TLS settings, Caddy creates a catch-all policy (`basePolicy`) and populates its `Issuers` slice from `caddytls.DefaultIssuersProvisioned` (line 95 in [`modules/caddytls/values.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/values.go)). By default, this instantiates an `ACMEIssuer` configured for Let's Encrypt's production directory (`https://acme-v02.api.letsencrypt.org/directory`).

**CA configuration override** – When targeting ZeroSSL, users set the `CA` field on the `ACMEIssuer` to `https://acme.zerossl.com/v2/DV90`. The same policy creation logic applies, but the issuer template is modified to handle ZeroSSL's External Account Binding (EAB) requirements.

## ZeroSSL EAB Credential Generation

ZeroSSL's ACME endpoint requires **External Account Binding (EAB)** credentials. Caddy eliminates manual setup by automatically generating these credentials when the CA URL begins with `https://acme.zerossl.com/`.

In [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go), the `makeIssuerTemplate` function (lines 61-70) detects ZeroSSL URLs and injects a custom `NewAccountFunc`:

```go
if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") {
    template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
        if acmeIss.ExternalAccount != nil {
            return acct, nil
        }
        var err error
        acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct)
        return acct, err
    }
}

```

The `generateZeroSSLEABCredentials` function (lines 17-76) automates the credential exchange:

- POSTs the primary contact email to `https://api.zerossl.com/acme/eab-credentials-email`
- Receives `eab_kid` and `eab_hmac_key` from the ZeroSSL API
- Stores these in `acme.EAB` for the ACME transaction

This enables automatic HTTPS with ZeroSSL without user-supplied credentials.

## Phase 3: Certificate Management with automaticHTTPSPhase2

Once all servers are started, `App.automaticHTTPSPhase2` (lines 0-22 in [`autohttps.go`](https://github.com/caddyserver/caddy/blob/main/autohttps.go)) initiates certificate procurement:

```go
app.logger.Info("enabling automatic TLS certificate management",
    zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)),
)
err := app.tlsApp.Manage(app.allCertDomains)

```

The `tlsApp.Manage` method invokes CertMagic to:

1. Look up the automation policies created during Phase 2
2. Execute the ACME protocol (or ZeroSSL API) to obtain or renew certificates
3. Store certificates in the configured storage (local disk by default)

Because policies already contain the appropriate issuer configuration, the same code path handles both Let's Encrypt and ZeroSSL ACME endpoints transparently.

## ACME vs ZeroSSL API Implementation Differences

Caddy supports three distinct issuer implementations for certificate authorities:

| Issuer Type | Protocol | Configuration | EAB Handling |
|-------------|----------|---------------|--------------|
| **Let's Encrypt** | Standard ACME (RFC 8555) | Default, no config required | Not required |
| **ZeroSSL via ACME** | Standard ACME with EAB | `ca https://acme.zerossl.com/v2/DV90` | Auto-generated via `generateZeroSSLEABCredentials` |
| **ZeroSSL via API** | Proprietary ZeroSSL REST API | `tls.issuance.zerossl { api_key ... }` | API key authentication |

The **ZeroSSLIssuer** in [`modules/caddytls/zerosslissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/zerosslissuer.go) provides the proprietary API integration (lines 76-99), but this path requires explicit configuration and manual API key management, bypassing the automatic HTTPS flow.

## Caddyfile Configuration Examples

### Let's Encrypt (Default)

```caddy
example.com {
    # Automatic HTTPS enabled by default

    respond "Hello, world!"
}

```

*Result:* Caddy discovers `example.com`, creates an ACME policy with Let's Encrypt's production endpoint, completes the HTTP-01 or TLS-ALPN-01 challenge, and serves HTTPS.

### ZeroSSL via ACME with Automatic EAB

```caddy
{
    tls {
        issuance acme {
            ca https://acme.zerossl.com/v2/DV90
            email admin@example.com  # Required for EAB generation

        }
    }
}

zerossl.example.com {
    respond "Secure with ZeroSSL"
}

```

*Result:* During Phase 1, Caddy registers `zerossl.example.com`. During Phase 2, `ACMEIssuer` detects the ZeroSSL CA URL, calls `generateZeroSSLEABCredentials` to obtain EAB credentials, and completes the ACME challenge flow identical to Let's Encrypt.

### ZeroSSL via Legacy API (Non-ACME)

```caddy
{
    tls {
        issuance zerossl {
            api_key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
            validity_days 90
        }
    }
}

api.example.com {
    reverse_proxy localhost:8080
}

```

*Result:* The `ZeroSSLIssuer` contacts ZeroSSL's proprietary API directly to request certificates, bypassing ACME entirely. This requires explicit TLS app configuration and manual API key provision, and is not part of the automatic HTTPS discovery flow.

## Summary

- **Automatic HTTPS operates in three phases**: hostname discovery (`automaticHTTPSPhase1`), automation policy creation (`createAutomationPolicies`), and certificate management (`automaticHTTPSPhase2`).
- **Both CAs use the same code path**: The `ACMEIssuer` in [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go) handles both Let's Encrypt and ZeroSSL ACME endpoints, differing only in CA URL and EAB requirements.
- **ZeroSSL EAB is automatic**: When the CA URL matches `https://acme.zerossl.com/*`, Caddy automatically generates EAB credentials via `generateZeroSSLEABCredentials` without user intervention.
- **Legacy API available**: A separate `ZeroSSLIssuer` exists for ZeroSSL's proprietary API in [`modules/caddytls/zerosslissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/zerosslissuer.go), but requires manual configuration and does not participate in automatic HTTPS discovery.

## Frequently Asked Questions

### What is the difference between ZeroSSL ACME and ZeroSSL API issuers in Caddy?

The **ZeroSSL ACME issuer** uses standard RFC 8555 ACME protocol through [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go), automatically generating EAB credentials and supporting the full automatic HTTPS flow. The **ZeroSSL API issuer** uses ZeroSSL's proprietary REST API through [`modules/caddytls/zerosslissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/zerosslissuer.go), requires manual API key configuration, and operates outside the automatic hostname discovery system.

### Does Caddy require manual EAB credentials for ZeroSSL?

No. When using ZeroSSL's ACME endpoint (`https://acme.zerossl.com/v2/DV90`), Caddy automatically generates the required External Account Binding credentials via the `generateZeroSSLEABCredentials` function. You only need to provide an email address in the TLS configuration; Caddy handles the API call to ZeroSSL's `eab-credentials-email` endpoint.

### Which source files control Caddy's automatic HTTPS behavior?

The primary files are:
- [`modules/caddyhttp/autohttps.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/autohttps.go) – Contains `automaticHTTPSPhase1` (hostname discovery), `createAutomationPolicies`, and `automaticHTTPSPhase2` (certificate management)
- [`modules/caddytls/acmeissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/acmeissuer.go) – Implements `ACMEIssuer` with `makeIssuerTemplate` and ZeroSSL EAB generation logic
- [`modules/caddytls/zerosslissuer.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/zerosslissuer.go) – Implements the proprietary ZeroSSL API issuer
- [`modules/caddytls/values.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddytls/values.go) – Defines `DefaultIssuersProvisioned` containing default Let's Encrypt configuration

### Can I use both Let's Encrypt and ZeroSSL simultaneously?

Yes. You can define multiple automation policies in your Caddy configuration, specifying different issuers for different site blocks. Caddy's `createAutomationPolicies` function builds separate CertMagic policies for each configured TLS context, allowing some sites to use Let's Encrypt while others use ZeroSSL's ACME endpoint or legacy API.