Files
certctl/docs/reference/connectors/acme.md
T
shankar0123 d809874fa1 docs: retire compliance subtree + sweep framework name-drops from prose
Per operator decision the framework-mapping docs are gone. They
were aspirational (no audit, no certification, no validated
mapping); keeping them around was misleading.

Files deleted (1,883 lines):
- docs/compliance/index.md
- docs/compliance/soc2.md
- docs/compliance/pci-dss.md
- docs/compliance/nist-sp-800-57.md

Hyperlinks removed:
- README.md: 'Auditor / compliance' row in the doc table; the
  '(compliance mapping included)' parenthetical in the
  positioning paragraph
- docs/README.md: the '## Compliance' section table; the
  'Auditor / compliance team' reading-order-by-role row

Prose name-drops swept across 24 files:
- README.md: 'FedRAMP boundary CAs / financial-services policy
  CAs' → '4-level boundary CAs / 3-level policy CAs';
  'Compliance-grade for PCI-DSS Level 1, FedRAMP Moderate / High,
  SOC 2 Type II, HIPAA' → cut entirely
- getting-started/{quickstart,concepts,examples,why-certctl,
  advanced-demo}.md: 'compliance' → 'audit' / 'policy';
  'PCI-DSS / SOC 2 / NIST SP 800-57' framework lists cut;
  ''pci': 'true'' tag example → ''environment': 'production''
- migration/cert-manager-coexistence.md: 'compliance rules' →
  'policy rules'
- operator/approval-workflow.md: 'Compliance customers (PCI-DSS
  Level 1, FedRAMP Moderate / High, SOC 2 Type II, HIPAA)' →
  'Operators'; entire 'Compliance control mapping' table
  (PCI-DSS §6.4.5 / NIST SP 800-53 SA-15 / SOC 2 Type II CC6.1
  / HIPAA §164.308(a)(4)) deleted; 'compliance contract' →
  'two-person-integrity contract'; 'compliance auditors' →
  'reviewers'
- operator/legacy-clients-tls-1.2.md: 'PCI-DSS v4.0 Req 4 §2.2.5'
  audit-reference → CWE-326 (kept); 'PCI-DSS Req 4 §2.2.5
  attestation' section retitled to 'TLS posture summary' and
  rewritten without framework framing; 'PCI-DSS, NIST, and
  major browsers will eventually deprecate TLS 1.2' →
  'Major browsers and OS vendors will eventually deprecate
  TLS 1.2'
- operator/database-tls.md: PCI-DSS Req 4 §2.2.5 audit-ref →
  CWE-319 only; 'PCI-DSS scope' → 'sensitive data'; PCI-DSS
  Req 4 v4.0 prose footing → cut
- operator/runbooks/disaster-recovery.md: 'SOC 2 / PCI
  procurement-team deliverable' → 'on-call deliverable';
  'compliance auditors' → 'reviewers'
- reference/connectors/{acme,aws-acm,azure-kv,globalsign,
  local-ca,openssl,ssh,index}.md: 'compliance reporting
  (PCI-DSS §3.6, HIPAA §164.312)' → 'audit reporting';
  'Compliance environments (PCI-DSS Level 1, FedRAMP High,
  HIPAA)' → 'Regulated environments'; 'compliance audits' →
  'audit'; 'FedRAMP boundary CA' pattern names →
  '4-level boundary CA' (technically descriptive)
- reference/protocols/est.md: 'compliance-hook seam' →
  'device-state hook seam'; 'compliance gating' → 'device-state
  gating'; 'est_compliance_failed' → 'est_device_state_failed'
- reference/protocols/scep-intune.md: 'Optional compliance
  check' → 'Optional device-state check'; failure-counter
  'compliance_failed' → 'device_state_failed'; 'Conditional
  Access compliance gating' → 'Conditional Access
  device-state gating'
- reference/intermediate-ca-hierarchy.md: 'FedRAMP boundary-CA
  deployments where the regulator requires...' →
  'Boundary-CA deployments where you want separation of policy
  and issuing authorities'; pattern A retitled '4-level FedRAMP
  boundary CA' → '4-level boundary CA'
- reference/architecture.md: broken Related-docs link to
  compliance.md removed; the rest of that block had stale
  pre-Phase-2 paths (quickstart.md, demo-advanced.md,
  connectors.md, openapi.md, testing-guide.md, test-env.md) —
  retargeted to current locations
- reference/deployment-model.md: 'SOC 2 evidence-report
  generator' → 'Audit-evidence report generator'
- reference/vendor-matrix.md: 'SOC 2 / PCI auditors paste this
  into evidence packs' → 'reviewers paste this into
  vendor-evaluation packs'
- contributor/qa-test-suite.md: 'compliance exist' coverage
  description cut; 'Compliance (PCI / SOC2 / HIPAA-relevant)'
  risk-class label → 'Audit-relevant'

What was kept:
- CWE references (legitimate technical pointers)
- Microsoft API/feature names that happen to use 'compliance'
  literally ('Microsoft Graph compliance API',
  'device-compliance validators' — these are MS product names,
  not framework name-drops)
- 'NIST PQC' on the landing page (Post-Quantum Cryptography is
  the actual NIST standard family, not a compliance framework)

Verified: zero hyperlinks into docs/compliance/ remain. All 24
ci-guards/*.sh pass locally. qa-doc-seed-count.sh clean.
Net diff: 26 files / -1,883 deletions in compliance/ + -32 net
across the prose sweep.

Companion edits in cowork/ (CLAUDE.md doc-tree summary +
WORKSPACE-CHANGELOG.md retirement note) land separately.
2026-05-05 05:26:44 +00:00

236 lines
8.8 KiB
Markdown

# ACME Issuer Connector — Operator Deep-Dive
> Last reviewed: 2026-05-05
>
> Operator-grade documentation for the outbound ACME v2 issuer
> connector (certctl as an ACME *client*). For the inbound ACME
> server (certctl as an ACME *server*), see
> [acme-server.md](../protocols/acme-server.md). For the
> connector-development context (interface contract, registry,
> ports/adapters), see the [connector index](index.md).
## Overview
The ACME connector implements the full ACME v2 protocol (RFC 8555)
using Go's `golang.org/x/crypto/acme` package. It supports three
challenge methods and ARI (RFC 9773) for renewal-window negotiation.
Compatible CAs include Let's Encrypt, ZeroSSL, Sectigo, Buypass,
Google Trust Services, SSL.com, and any other RFC 8555 ACME
implementation. step-ca's ACME directory is also compatible if you
prefer ACME over the native step-ca connector.
Implementation lives at `internal/connector/issuer/acme/`.
## When to use this connector
Use the ACME connector when:
- You need public-trust certificates (Let's Encrypt, ZeroSSL,
Sectigo via ACME, Google Trust Services, SSL.com).
- You want certctl to drive renewal lifecycle on top of the ACME
CA's free or paid issuance.
- You want one tool that covers both internal PKI (Local, Vault,
step-ca) and public-trust ACME issuance.
Look elsewhere when:
- You need OV / EV certificates and your CA doesn't expose them
via ACME — use the DigiCert or Sectigo SCM REST connectors.
- You're standing up internal-only PKI and don't want to operate
ACME challenge infrastructure — use Local CA or Vault PKI for a
simpler synchronous path.
## Challenge methods
### HTTP-01 (default)
A built-in temporary HTTP server starts on demand during
certificate issuance. The domain being validated must resolve to
the machine running the connector, and the configured HTTP port
must be reachable from the internet.
```json
{
"directory_url": "https://acme-staging-v02.api.letsencrypt.org/directory",
"email": "admin@example.com",
"http_port": 80
}
```
### DNS-01 (for wildcards)
Creates DNS TXT records via user-provided scripts. Required for
wildcard certificates (`*.example.com`) and hosts that can't serve
HTTP on port 80. The connector invokes external scripts to create
and clean up `_acme-challenge` TXT records, making it compatible
with any DNS provider (Cloudflare, Route53, Azure DNS, etc.).
```json
{
"directory_url": "https://acme-v02.api.letsencrypt.org/directory",
"email": "admin@example.com",
"challenge_type": "dns-01",
"dns_present_script": "/etc/certctl/dns/create-record.sh",
"dns_cleanup_script": "/etc/certctl/dns/delete-record.sh",
"dns_propagation_wait": 30
}
```
DNS hook scripts receive these environment variables:
- `CERTCTL_DNS_DOMAIN` — domain being validated
- `CERTCTL_DNS_FQDN` — full record name (`_acme-challenge.<domain>`
for dns-01, `_validation-persist.<domain>` for dns-persist-01)
- `CERTCTL_DNS_VALUE` — TXT record value
- `CERTCTL_DNS_TOKEN` — ACME challenge token
The present script must create the TXT record and exit 0; the
cleanup script removes it (dns-01 only).
### DNS-PERSIST-01 (standing record)
Creates a one-time persistent TXT record at
`_validation-persist.<domain>` containing the CA's issuer domain
and your ACME account URI. Once set, this record authorizes
unlimited future certificate issuances without per-renewal DNS
updates. Based on
[draft-ietf-acme-dns-persist](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
and CA/Browser Forum ballot SC-088v3.
If the CA doesn't offer dns-persist-01 yet, the connector falls
back to dns-01 automatically.
```json
{
"directory_url": "https://acme-v02.api.letsencrypt.org/directory",
"email": "admin@example.com",
"challenge_type": "dns-persist-01",
"dns_present_script": "/etc/certctl/dns/create-record.sh",
"dns_persist_issuer_domain": "letsencrypt.org",
"dns_propagation_wait": 30
}
```
The present script creates a TXT record at
`_validation-persist.<domain>` with the value
`letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/<your-id>`.
This record is permanent — no cleanup script is needed.
## ACME Renewal Information (ARI, RFC 9773)
Instead of using fixed renewal thresholds (e.g. renew 30 days
before expiry), certctl can ask the CA when it should renew.
Enable with `CERTCTL_ACME_ARI_ENABLED=true`.
The ARI protocol lets the CA specify a `suggestedWindow` (start
and end times) for when you should renew — useful for distributing
load during maintenance windows or coordinating mass-revocation
scenarios. Cert ID is computed as `base64url(SHA-256(DER cert))`.
If the CA doesn't support ARI (404 response), certctl
automatically falls back to threshold-based renewal with no
operator intervention required.
## External Account Binding (EAB)
ZeroSSL, Google Trust Services, and SSL.com require EAB for ACME
account registration. For most CAs, get your EAB credentials from
the CA's dashboard and provide them via `eab_kid` and `eab_hmac`.
The HMAC key must be base64url-encoded (no padding). CAs that
don't require EAB (Let's Encrypt, Buypass) ignore these fields.
```json
{
"directory_url": "https://acme.zerossl.com/v2/DV90",
"email": "admin@example.com",
"eab_kid": "your-zerossl-eab-kid",
"eab_hmac": "your-zerossl-eab-hmac-base64url"
}
```
### ZeroSSL auto-EAB
When the directory URL points to ZeroSSL and no EAB credentials
are provided, certctl automatically fetches them from ZeroSSL's
public API (`api.zerossl.com/acme/eab-credentials-email`) using
your configured email address. No dashboard visit required — just
set the directory URL and email. Same approach used by Caddy and
acme.sh.
```json
{
"directory_url": "https://acme.zerossl.com/v2/DV90",
"email": "admin@example.com"
}
```
## Certificate profiles (Let's Encrypt, GA January 2026)
Let's Encrypt supports ACME certificate profile selection. Set
`CERTCTL_ACME_PROFILE=shortlived` to request 6-day certificates —
ideal for ephemeral workloads where short validity substitutes for
revocation. The `tlsserver` profile produces standard TLS
certificates. When the profile field is empty (default), the CA
uses its default profile.
## Environment variables
- `CERTCTL_ACME_DIRECTORY_URL` — ACME directory URL
- `CERTCTL_ACME_EMAIL` — Contact email for account registration
- `CERTCTL_ACME_EAB_KID` — External Account Binding Key ID
- `CERTCTL_ACME_EAB_HMAC` — External Account Binding HMAC key
(base64url-encoded)
- `CERTCTL_ACME_CHALLENGE_TYPE``http-01` (default), `dns-01`,
or `dns-persist-01`
- `CERTCTL_ACME_DNS_PRESENT_SCRIPT` — Path to DNS record creation
script
- `CERTCTL_ACME_DNS_CLEANUP_SCRIPT` — Path to DNS record cleanup
script (dns-01 only)
- `CERTCTL_ACME_DNS_PERSIST_ISSUER_DOMAIN` — CA issuer domain for
persistent record (dns-persist-01 only)
- `CERTCTL_ACME_PROFILE` — Certificate profile for the newOrder
request
## Revocation by serial number (Top-10 fix #7)
RFC 8555 §7.6 requires the certificate DER bytes (not just the
serial) on the revoke wire — but a CLM platform's job is to
abstract over that limitation. Operators routinely have only the
serial in hand: the original PEM was lost, the private key was
rotated, the operator clicked "revoke" in the GUI based on a row
in the certs list.
certctl's ACME
`RevokeCertificate(ctx, RevocationRequest{Serial: ...})` looks the
serial up in the local cert store
(`certificate_versions.pem_chain`), decodes the leaf-cert PEM into
DER, and calls the ACME revoke endpoint with
`(accountKey, der, reasonCode)` — RFC 8555 §7.6 case 1,
"revocation request signed with account key". This works because
the same account key issued the cert, so authority is intrinsic.
The cert version must exist in the local store: this means the
cert was issued through certctl, not imported. If
`GetVersionBySerial` returns `sql.ErrNoRows`, the connector
returns an actionable error pointing at the local-store
requirement. Revoke-by-serial is therefore only available for
ACME certs that certctl issued.
Reason codes follow RFC 5280 §5.3.1: nil reason maps to
`unspecified` (0), and the connector accepts the canonical
camelCase form (`keyCompromise`, `cACompromise`,
`affiliationChanged`, `superseded`, `cessationOfOperation`,
`certificateHold`, `removeFromCRL`, `privilegeWithdrawn`,
`aACompromise`) plus underscore_lower and ALL_CAPS_UNDERSCORE
variants. An unknown reason returns an error rather than silently
demoting to `unspecified` — operators rely on the reason for
audit reporting.
## Related docs
- [ACME server](../protocols/acme-server.md) — certctl *as* an ACME server (the inverse direction)
- [Connector index](index.md) — interface contract, registry, port/adapter wiring
- [migration/acme-from-cert-manager.md](../../migration/acme-from-cert-manager.md) — point cert-manager at certctl's ACME server
- [migration/acme-from-traefik.md](../../migration/acme-from-traefik.md) — point Traefik at certctl's ACME server