Per Phase 1 audit at cowork/docs-overhaul-phase-1-audit-2026-05-04/.
Continuation of Phase 11 (commit dca1900 handled README + first round
of docs/ links). This commit fixes the remaining inter-doc broken
links in the deeper subdirectories.
Per source directory:
docs/getting-started/quickstart.md (1 fix):
(connectors.md) → (../reference/connectors/index.md)
docs/contributor/test-environment.md (2 fixes):
(tls.md) → (../operator/tls.md)
(upgrade-to-tls.md) → (../archive/upgrades/to-tls-v2.2.md)
docs/contributor/testing-strategy.md (4 fixes):
`docs/security.md` → `docs/operator/security.md`
(security.md) → (../operator/security.md)
`docs/testing-guide.md` (kept; testing-guide.md still at top level
pending Phase 5 prune)
(testing-guide.md) → (../testing-guide.md)
docs/migration/acme-from-traefik.md (2 sites, multi-link):
(./acme-cert-manager-walkthrough.md) → (./acme-from-cert-manager.md)
(./acme-server.md) → (../reference/protocols/acme-server.md)
docs/migration/cert-manager-coexistence.md (1 fix):
(./quickstart.md) → (../getting-started/quickstart.md)
docs/migration/from-acmesh.md (2 fixes):
(connectors.md) → (../reference/connectors/index.md)
(./examples.md) → (../getting-started/examples.md)
docs/migration/acme-from-caddy.md (multi-link):
(./acme-cert-manager-walkthrough.md) → (./acme-from-cert-manager.md)
(./acme-server.md) → (../reference/protocols/acme-server.md)
docs/migration/acme-from-cert-manager.md (multi-link):
(./acme-server.md) → (../reference/protocols/acme-server.md)
(./acme-server-threat-model.md) → (../reference/protocols/acme-server-threat-model.md)
(./acme-caddy-walkthrough.md) → (./acme-from-caddy.md)
(./acme-traefik-walkthrough.md) → (./acme-from-traefik.md)
docs/migration/from-certbot.md (2 fixes):
(./concepts.md) → (../getting-started/concepts.md)
(./examples.md) → (../getting-started/examples.md)
docs/operator/tls.md (3 sites):
(upgrade-to-tls.md) → (../archive/upgrades/to-tls-v2.2.md)
(quickstart.md) → (../getting-started/quickstart.md)
(test-env.md) → (../contributor/test-environment.md)
docs/operator/runbooks/disaster-recovery.md (5 fixes):
(crl-ocsp.md) → (../../reference/protocols/crl-ocsp.md)
(tls.md) → (../../operator/tls.md)
(security.md) → (../../operator/security.md)
(scep-intune.md) → (../../reference/protocols/scep-intune.md)
(est.md) → (../../reference/protocols/est.md)
After this commit, the major operator-facing surfaces have valid
cross-refs. Some lower-traffic docs (compliance/soc2.md, compliance/
nist-sp-800-57.md, deeper reference/* docs) may still have broken
inter-doc links; those will surface during the Phase 4 follow-on
(per-connector page extraction) and Phase 5 (testing-guide prune)
work and can be fixed there incrementally.
6.7 KiB
Traefik Integration Walkthrough
Last reviewed: 2026-05-05
Use this walkthrough when you're already running Traefik 3.0+ (Kubernetes or VM) 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 Traefik static config changes are minimal; the load-bearing piece is
serversTransport.rootCAsso Traefik trusts certctl's bootstrap CA on every outbound ACME call.
End-to-end recipe for issuing certs from a certctl-server deployment through Traefik 3.0+. Target audience: operator running Traefik (in Kubernetes or on a VM) who wants to use certctl as their ACME source of truth 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. - Traefik 3.0+ (the v2 API surface for ACME is also supported but the
serversTransport.rootCAsreference below is v3-shaped). - The certctl bootstrap CA, in PEM form, captured the same way as the cert-manager walkthrough Step 3.
Step 1 — Configure Traefik static config
Traefik's ACME issuer is a certificatesResolver in the static config
(file or CLI flags or env vars). The relevant fields:
# /etc/traefik/traefik.yml (or wherever your static config lives)
certificatesResolvers:
certctl:
acme:
caServer: https://certctl.example.com:8443/acme/profile/prof-test/directory
email: ops@example.com
storage: /etc/traefik/acme-certctl.json
httpChallenge:
entryPoint: web
# OR for trust_authenticated mode profiles:
# tlsChallenge: {}
# certctl uses a self-signed bootstrap cert; Traefik needs the CA
# explicitly via serversTransport.rootCAs to call the directory URL.
serversTransports:
default:
rootCAs:
- /etc/traefik/certctl-bootstrap.crt
# Apply the serversTransport globally so every outbound HTTPS call —
# including ACME directory + finalize — trusts the certctl CA.
api:
insecure: false
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
Notes:
caServermust point at the directory URL (ending in/directory).httpChallenge.entryPoint: webrequires Traefik'swebentryPoint (port 80) to be reachable from certctl-server's HTTP-01 validator. Fortrust_authenticatedmode profiles, this is a no-op formality — certctl auto-resolves authzs, so the solver round-trip never happens.tlsChallenge: {}is the alternative that uses TLS-ALPN-01 (RFC 8737) via Traefik'swebsecure(port 443) entryPoint. Either works underchallengemode; only the default-of-tlsChallengeis recommended fortrust_authenticatedmode.
Step 2 — Trust the certctl bootstrap CA
Two options:
Option A — serversTransport.rootCAs (preferred)
sudo cp deploy/test/certs/ca.crt /etc/traefik/certctl-bootstrap.crt
sudo systemctl reload traefik
serversTransports.default.rootCAs (shown in Step 1 above) tells
Traefik's outbound HTTPS client to trust the supplied PEM in addition
to the system trust store. This is the right pattern for containerized
Traefik where you don't want to install OS-level trust roots.
Option B — OS trust store
For Traefik running directly on a VM, update-ca-certificates-style
installation works the same way as the Caddy walkthrough Option A.
The serversTransport.rootCAs field is unnecessary in that case.
Step 3 — Reference the resolver from a router
Per-router (dynamic config):
# /etc/traefik/dynamic/example-com.yml
http:
routers:
example-com:
rule: "Host(`example.com`)"
entryPoints: [websecure]
tls:
certResolver: certctl
service: example-com-backend
services:
example-com-backend:
loadBalancer:
servers:
- url: "http://localhost:8080"
Or, in Kubernetes via IngressRoute (Traefik CRD):
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: example-com
spec:
entryPoints: [websecure]
routes:
- match: Host(`example.com`)
kind: Rule
services:
- name: example-com-backend
port: 8080
tls:
certResolver: certctl
Step 4 — Reload Traefik
sudo systemctl reload traefik
# OR kubectl rollout restart deployment/traefik (if you changed the static config via ConfigMap).
On the first request to example.com, Traefik hits certctl's directory
URL, registers an account, submits a new-order, and finalizes. The cert
is persisted to /etc/traefik/acme-certctl.json (or its in-cluster
PVC equivalent).
Step 5 — Verify
curl -kvI https://example.com 2>&1 | grep -E 'subject|issuer'
# subject: CN=example.com
# issuer: CN=certctl test internal CA
The cert is signed by certctl's bound issuer (per the prof-test
profile's issuer_id).
On the certctl side, the audit log captures the issuance:
psql -c "SELECT actor, action, resource_id FROM audit_events
WHERE actor LIKE 'acme:%' ORDER BY created_at DESC LIMIT 5;"
Common failure modes
- Traefik logs
unable to obtain ACME certificate ... x509: certificate signed by unknown authority→serversTransport.rootCAsis not pointing at the certctl bootstrap CA, OR the file was rotated and Traefik hasn't reloaded. Verify withcurl --cacert /etc/traefik/certctl-bootstrap.crt https://certctl.example.com:8443/acme/profile/prof-test/directory. - Traefik logs
urn:ietf:params:acme:error:rateLimited→ tuneCERTCTL_ACME_SERVER_RATE_LIMIT_ORDERS_PER_HOURon the certctl side, OR reduce Traefik's parallel-cert-acquisition concurrency. acme: error: 400 :: POST :: ... :: badNonce→ clock skew or multi-replica certctl without sticky sessions; same fix as the cert-manager walkthrough.- Storage file
acme-certctl.jsonshows persistent failures — Traefik retains failed-acquisition state. After fixing the underlying cause, delete the storage file and reload:rm /etc/traefik/acme-certctl.json && systemctl reload traefik.
Cleanup
# Remove the certResolver from any router / IngressRoute consuming it.
sudo systemctl reload traefik
# Delete the persisted ACME storage:
sudo rm /etc/traefik/acme-certctl.json
# Or in K8s: drop the resolver from the static-config ConfigMap.
See also
docs/acme-server.md— canonical reference.docs/acme-cert-manager-walkthrough.md— cert-manager equivalent.- Traefik upstream ACME docs — verify behavior pinned here against Traefik 3.0+ semantics.