Per Phase 1 audit at cowork/docs-overhaul-phase-1-audit-2026-05-04/.
Sweeps the anchor-bearing inter-doc links that the previous Phase 11
sed pass missed (anchors after .md# weren't matched), plus a few
remaining cross-refs in docs/reference/.
Per source file:
docs/migration/acme-from-caddy.md (1 anchor link):
(./acme-server.md#certificate-readyfalse-with-rejectedidentifier)
→ (../reference/protocols/acme-server.md#certificate-readyfalse-...)
docs/migration/acme-from-cert-manager.md (3 anchor links):
Same shape; all (./acme-server.md#...) → (../reference/protocols/acme-server.md#...)
docs/reference/connectors/index.md (5 walkthrough + reference links):
(./acme-server.md) → (../protocols/acme-server.md)
(./acme-server-threat-model.md) → (../protocols/acme-server-threat-model.md)
(./acme-cert-manager-walkthrough.md) → (../../migration/acme-from-cert-manager.md)
(./acme-caddy-walkthrough.md) → (../../migration/acme-from-caddy.md)
(./acme-traefik-walkthrough.md) → (../../migration/acme-from-traefik.md)
docs/reference/protocols/acme-server.md (3 walkthrough links):
(./acme-cert-manager-walkthrough.md) → (../../migration/acme-from-cert-manager.md)
(./acme-caddy-walkthrough.md) → (../../migration/acme-from-caddy.md)
(./acme-traefik-walkthrough.md) → (../../migration/acme-from-traefik.md)
docs/reference/protocols/acme-server-threat-model.md (1 cross-dir):
(./tls.md) → (../../operator/tls.md)
After this commit, every grep for old-style `./<old-doc-name>.md` links
returns clean across docs/migration/, docs/reference/, and
docs/operator/.
6.2 KiB
Caddy Integration Walkthrough
Last reviewed: 2026-05-05
Use this walkthrough when you're already running Caddy 2.7+ and want it to ACME-issue from certctl (your internal CA, your private PKI, or a local sub-CA chained under an enterprise root) instead of Let's Encrypt. The Caddyfile changes are minimal; the load-bearing piece is trusting certctl's bootstrap CA so Caddy's ACME client can talk to certctl over HTTPS.
End-to-end recipe for issuing certs from a certctl-server deployment through Caddy 2.7+. Target audience: operator running Caddy on a VM or container who wants Caddy to ACME-issue from certctl instead of Let's Encrypt.
Prereqs
- A reachable certctl-server with
CERTCTL_ACME_SERVER_ENABLED=trueand at least one profile whoseacme_auth_modeis set. Profile setup is identical to the cert-manager walkthrough — seedocs/acme-cert-manager-walkthrough.mdStep 2. - Caddy 2.7.x or later.
caddy versionshould show 2.7.0+. - Network reachability: Caddy → certctl-server's HTTPS listener (port 8443 by default).
- The certctl bootstrap CA, in PEM form, captured for the trust
configuration below. Capture exactly the same way as the cert-manager
walkthrough Step 3 — use
cat deploy/test/certs/ca.crt.
Step 1 — Configure Caddy
Caddy's ACME issuer is configured per-site (or globally) via the
acme_ca directive in a Caddyfile, or via the tls.acme_ca field
in JSON config. The directive points at the directory URL:
{
email ops@example.com
}
example.com {
tls {
acme_ca https://certctl.example.com:8443/acme/profile/prof-test/directory
issuer acme
}
reverse_proxy localhost:8080
}
Notes:
acme_camust point at the directory URL (ending in/directory), not just the base. Caddy uses the directory document to discover the new-account / new-order URLs, exactly the same way cert-manager does.issuer acmeis the default; included here for clarity. Caddy can also be configured withissuer zerosslorissuer internal; for certctl integration,acmeis the correct issuer.- Caddy auto-discovers
tls-alpn-01first when port 443 is bound to Caddy, then falls back to HTTP-01. Fortrust_authenticatedmode profiles, both work without solver round-trips.
Step 2 — Trust the certctl bootstrap CA
Caddy validates the certctl-server's TLS chain before any ACME call, the same way cert-manager does. Two options for trust:
Option A — OS trust store (preferred for VMs)
sudo cp deploy/test/certs/ca.crt /usr/local/share/ca-certificates/certctl-bootstrap.crt
sudo update-ca-certificates
sudo systemctl restart caddy
Caddy honors the system trust store via the Go runtime's
crypto/x509 defaults. After update-ca-certificates, Caddy's HTTPS
client trusts certctl's self-signed root and the directory call
succeeds.
Option B — Caddy tls.cas (for containerized deployments)
{
pki {
ca certctl_bootstrap {
root_cert_file /etc/caddy/certctl-bootstrap.crt
}
}
}
example.com {
tls {
acme_ca https://certctl.example.com:8443/acme/profile/prof-test/directory
ca certctl_bootstrap
issuer acme
}
reverse_proxy localhost:8080
}
The pki.ca block registers a named CA Caddy can reference; the
tls.ca certctl_bootstrap line in the site block scopes that trust
to ACME calls for this site only. This is the right pattern for
multi-tenant Caddy deployments where some sites trust certctl + others
don't.
Step 3 — Reload Caddy
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Caddy reloads atomically; in-flight requests complete on the old
config while new requests use the new ACME issuer. On the next
example.com request, Caddy hits certctl's directory URL, registers
an account, submits a new-order, and finalizes — typically completing
in under 5 seconds for trust_authenticated mode.
Step 4 — Verify
caddy list-certificates
# example.com (issuer=certctl.example.com): CN=example.com, valid until 2026-06-30
The cert is in Caddy's certificate cache ($XDG_DATA_HOME/caddy/certificates/
by default). Inspect:
openssl x509 -in ~/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com/example.com.crt -noout -subject -issuer -dates
# subject= CN=example.com
# issuer= CN=certctl test internal CA
(Path layout is Caddy-version-dependent; check caddy environ for the
canonical data dir.)
On the certctl side, the operator's audit log captures the issuance event:
psql -c "SELECT actor, action, resource_id FROM audit_events
WHERE actor LIKE 'acme:%' ORDER BY created_at DESC LIMIT 5;"
Common failure modes
- Caddy logs
tls: failed to verify certificate: x509: certificate signed by unknown authority→ certctl bootstrap CA is not in Caddy's trust path. Re-do Step 2; verify withcurl --cacert /etc/caddy/certctl-bootstrap.crt https://certctl.example.com:8443/acme/profile/prof-test/directory. - Caddy logs
urn:ietf:params:acme:error:rateLimited→ certctl per-account orders/hour limit hit (default 100/hr). Tune viaCERTCTL_ACME_SERVER_RATE_LIMIT_ORDERS_PER_HOURif you have legitimately high throughput. - Caddy logs
urn:ietf:params:acme:error:rejectedIdentifier→ the SAN list includes an identifier the certctl profile policy rejects. Cross-referencedocs/acme-server.md§ Troubleshooting. badNoncein Caddy logs → clock skew or multi-replica certctl without sticky sessions; same fix as the cert-manager walkthrough.
Cleanup
caddy stop
# remove the certctl-specific block from your Caddyfile
sudo systemctl reload caddy
# Optional: delete cached certs from the certctl directory namespace.
rm -rf ~/.local/share/caddy/certificates/certctl.example.com-*
See also
docs/acme-server.md— canonical reference.docs/acme-cert-manager-walkthrough.md— K8s-native equivalent.- Caddy upstream ACME docs — verify behavior pinned here against Caddy 2.7.x semantics.