mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 13:28:51 +00:00
39f065dda4
Doc-only commit closing the ACME-server work series. After this commit,
an outside reviewer (procurement engineer / Venafi diligence engineer /
Infisical-comparison-shopper) can read the docs cold, understand the
ACME server's surface, follow the cert-manager walkthrough, and reach
a deployment decision without escalating to certctl maintainers.
What ships:
- docs/acme-server.md final pass: Auth-mode decision tree (when to
use trust_authenticated vs challenge), RFC 8555 + RFC 9773
conformance statement (section-by-section table of implemented
plus procurement-honest 'not implemented' rows for EAB / multi-
level wildcards / RFC 8738 / cross-CA proxying), Troubleshooting
(5 failure modes — badNonce / unknownAuthority / HTTP-01
connection refused / DNS-01 NXDOMAIN / rejectedIdentifier with
canonical fix for each), Version pinning + tested clients table
(cert-manager 1.15.0, lego v4, kind v0.20+, Caddy 2.7.x, Traefik
3.0+), FAQ (5 entries — why two auth modes, vs cert-manager-
against-LE, can-I-use-from-outside-K8s, migration story, audit-
log catalog), See-also cross-link block.
- docs/acme-cert-manager-walkthrough.md: kind → cert-manager →
certctl → Certificate flow, with YAML blocks byte-equal to
deploy/test/acme-integration/{clusterissuer-trust-authenticated,
certificate-test}.yaml to prevent doc/test drift.
- docs/acme-caddy-walkthrough.md: Caddyfile acme_ca + tls.cas
options (OS trust store + Caddy pki.ca block).
- docs/acme-traefik-walkthrough.md: certificatesResolvers.<name>.acme
.caServer + serversTransport.rootCAs configuration.
- docs/acme-server-threat-model.md: Threat surface map + JWS forgery
resistance (alg-confusion / HS256 substitution / replayed nonce /
URL spoofing / multi-sig / kid-vs-jwk / kid round-trip mismatch),
Nonce store integrity rationale, HTTP-01 SSRF defense-in-depth
(pre-dial check + per-dial check + per-redirect check + body cap +
bounded redirects), DNS-01 cache-poisoning posture (default Google
Public DNS + operator-owns-private-resolver-posture), TLS-ALPN-01
chain-not-validated rationale (RFC 8737 §3 explicit), Rate-limit
tuning, Audit trail catalog, Out-of-scope threats list.
- docs/connectors.md: TOC renumbered 3→4 etc. to make room for new
top-level 'ACME Server (Built-in)' section between Issuer Connector
and Target Connector — distinguishes the consumer-side ACME
(existing) from the new server-side ACME via env-var-prefix
call-out (CERTCTL_ACME_* vs CERTCTL_ACME_SERVER_*).
DoD verification:
- All 5 docs files exist with the structure prescribed by the
Phase 6 prompt.
- Every CERTCTL_ACME_SERVER_* env var in docs/acme-server.md maps
to an actual lookup in internal/config/config.go (verified by
'grep -oE | sort -u | diff' returning empty).
- Every YAML snippet in docs/acme-cert-manager-walkthrough.md is
byte-equal to the corresponding file in deploy/test/acme-integration/
(verified with 'diff' against awk-extracted YAML blocks).
- docs/connectors.md has the cross-link subsection with all 4 new
docs referenced.
- cowork/CLAUDE.md Architecture Decisions has the new ACME-server
bullet documenting per-profile URL family + per-profile
acme_auth_mode + Phase 4-5-6 progression.
- cowork/WORKSPACE-CHANGELOG.md has the ACME-Server-6 entry plus
the ACME-Server rollup spanning Phases 1a-6.
- cowork/infisical-deep-research-results.md Rank 1 marked SHIPPED.
- 'gofmt -l .' clean (no Go changes); 'go vet ./...' clean.
Acquisition-readiness: every one of the 12 acquisition-grade criteria
from cowork/acme-server-endpoint-prompt.md is verified by the test
suite (Phases 1a-5) plus this doc walkthrough (Phase 6). The full
RFC 8555 + RFC 9773 surface is live; the operator can deploy
end-to-end by reading one walkthrough doc and one env-var table.
Engineering history: cowork/WORKSPACE-CHANGELOG.md 'ACME-Server-6 (docs)'
+ ACME-Server rollup of all 6 phases.
173 lines
5.8 KiB
Markdown
173 lines
5.8 KiB
Markdown
# Caddy Integration Walkthrough
|
|
|
|
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=true`
|
|
and at least one profile whose `acme_auth_mode` is set. Profile
|
|
setup is identical to the cert-manager walkthrough — see
|
|
[`docs/acme-cert-manager-walkthrough.md`](./acme-cert-manager-walkthrough.md)
|
|
Step 2.
|
|
- Caddy 2.7.x or later. `caddy version` should 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_ca` must 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 acme` is the default; included here for clarity. Caddy can
|
|
also be configured with `issuer zerossl` or `issuer internal`; for
|
|
certctl integration, `acme` is the correct issuer.
|
|
- Caddy auto-discovers `tls-alpn-01` first when port 443 is bound to
|
|
Caddy, then falls back to HTTP-01. For `trust_authenticated` mode
|
|
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 with `curl --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 via
|
|
`CERTCTL_ACME_SERVER_RATE_LIMIT_ORDERS_PER_HOUR` if 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-reference [`docs/acme-server.md` § Troubleshooting](./acme-server.md#certificate-readyfalse-with-rejectedidentifier).
|
|
- **`badNonce` in 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`](./acme-server.md) — canonical reference.
|
|
- [`docs/acme-cert-manager-walkthrough.md`](./acme-cert-manager-walkthrough.md) —
|
|
K8s-native equivalent.
|
|
- [Caddy upstream ACME docs](https://caddyserver.com/docs/automatic-https#acme-issuer)
|
|
— verify behavior pinned here against Caddy 2.7.x semantics.
|