diff --git a/docs/reference/connectors/acme.md b/docs/reference/connectors/acme.md new file mode 100644 index 0000000..67c1185 --- /dev/null +++ b/docs/reference/connectors/acme.md @@ -0,0 +1,235 @@ +# 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.` + for dns-01, `_validation-persist.` 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.` 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.` with the value +`letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/`. +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 +compliance reporting (PCI-DSS §3.6, HIPAA §164.312). + +## 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 diff --git a/docs/reference/connectors/entrust.md b/docs/reference/connectors/entrust.md new file mode 100644 index 0000000..7724dea --- /dev/null +++ b/docs/reference/connectors/entrust.md @@ -0,0 +1,96 @@ +# Entrust Certificate Services Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the Entrust CA Gateway issuer +> connector. For the connector-development context (interface +> contract, registry, ports/adapters), see the +> [connector index](index.md). + +## Overview + +The Entrust connector calls the Entrust CA Gateway REST API with +mutual TLS client-certificate authentication. It supports +synchronous issuance (200 OK with PEM) and approval-pending flows +(201 Accepted with async polling). + +Implementation lives at `internal/connector/issuer/entrust/` (the +mTLS keypair cache is shared at +`internal/connector/issuer/mtlscache/`). + +## When to use this connector + +Use the Entrust connector when: + +- You're an Entrust Certificate Services customer using the CA + Gateway as the integration surface. +- You need approval-pending workflows where humans approve + enrollments before issuance. +- You want mTLS-authenticated issuance against a commercial CA + with no API keys to rotate. + +Look elsewhere when: + +- You only need DV / OV public-trust and your CA is reachable via + ACME — use the [ACME connector](acme.md) for a simpler path. +- You're not already an Entrust customer — DigiCert, Sectigo, and + GlobalSign are comparable commercial alternatives, with + different auth shapes. + +## Configuration + +| Setting | Required | Default | Description | +|---|---|---|---| +| `CERTCTL_ENTRUST_API_URL` | Yes | — | Entrust CA Gateway base URL | +| `CERTCTL_ENTRUST_CLIENT_CERT_PATH` | Yes | — | Path to mTLS client certificate PEM | +| `CERTCTL_ENTRUST_CLIENT_KEY_PATH` | Yes | — | Path to mTLS client private key PEM | +| `CERTCTL_ENTRUST_CA_ID` | Yes | — | Certificate Authority ID (from `GET /certificate-authorities`) | +| `CERTCTL_ENTRUST_PROFILE_ID` | No | — | Optional enrollment profile ID | +| `CERTCTL_ENTRUST_POLL_MAX_WAIT_SECONDS` | No | `600` (10m) | Bounded-polling deadline for `GetOrderStatus` | + +For approval-pending workflows where humans approve enrollments, +bump `CERTCTL_ENTRUST_POLL_MAX_WAIT_SECONDS` to `86400` (24h) so a +single tick can wait through the approval window. + +## Authentication + +Mutual TLS — the client certificate and key are loaded via +`tls.LoadX509KeyPair()` and attached to the HTTP transport. No API +key or token required. + +## Issuance model + +Enrollment via +`POST /v1/certificate-authorities/{caId}/enrollments`. Returns 200 +with PEM immediately for auto-approved enrollments, or 201 +Accepted with a tracking ID for approval-pending orders. +`GetOrderStatus` polls the enrollment endpoint. + +## mTLS keypair caching (audit fix #10) + +The parsed client certificate plus a precomputed `*http.Transport` +are cached on the connector after the first API call. Steady-state +calls reuse the cached transport — no per-call disk read or +`tls.X509KeyPair` parse. + +Rotation is picked up automatically via mtime polling: when the +cert file's mtime advances beyond the last-loaded value, the next +API call re-parses and rebuilds the transport. + +Operator workflow: `mv -f new.crt /etc/certctl/entrust/client.crt` +(mtime changes), no process restart required, takes effect on the +next API call. `os.Stat` errors during rotation surface as +connector errors rather than silently serving stale credentials. + +## Revocation + +CRL and OCSP are managed by Entrust. certctl records revocations +locally and notifies Entrust via +`PUT /v1/certificate-authorities/{caId}/certificates/{serial}/revoke`. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [GlobalSign Atlas HVCA](globalsign.md) — comparable mTLS-authenticated commercial CA +- [Async CA polling](../protocols/async-ca-polling.md) — the bounded-polling primitive +- [Approval workflow](../../operator/approval-workflow.md) — certctl-side two-person integrity (separate from Entrust's approval queue) diff --git a/docs/reference/connectors/globalsign.md b/docs/reference/connectors/globalsign.md new file mode 100644 index 0000000..837c027 --- /dev/null +++ b/docs/reference/connectors/globalsign.md @@ -0,0 +1,122 @@ +# GlobalSign Atlas HVCA Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the GlobalSign Atlas High Volume +> CA (HVCA) issuer connector. For the connector-development context +> (interface contract, registry, ports/adapters), see the +> [connector index](index.md). + +## Overview + +GlobalSign Atlas HVCA REST API with **dual authentication**: mTLS +for the TLS handshake AND API key/secret headers for request +authorization. Region-aware base URLs (EMEA, APAC, Americas). + +Implementation lives at `internal/connector/issuer/globalsign/` +(mTLS keypair cache shared at +`internal/connector/issuer/mtlscache/`). + +## When to use this connector + +Use the GlobalSign Atlas HVCA connector when: + +- You're a GlobalSign Atlas customer issuing high volumes of + publicly trusted certificates (the "HV" in HVCA). +- You want region-pinned issuance for compliance or latency + reasons (EMEA / APAC / Americas regional endpoints). +- You're prepared to manage both mTLS client certs AND + API key/secret credentials in tandem. + +Look elsewhere when: + +- You only need DV public-trust and your CA is reachable via ACME — + the [ACME connector](acme.md) is simpler. +- The dual-auth burden (mTLS + API key + API secret) is heavier + than your environment needs — DigiCert (API key only) or Entrust + (mTLS only) are simpler to operate. + +## Configuration + +| Setting | Required | Default | Description | +|---|---|---|---| +| `CERTCTL_GLOBALSIGN_API_URL` | Yes | — | Atlas HVCA API URL (region-specific) | +| `CERTCTL_GLOBALSIGN_API_KEY` | Yes | — | API key for request authentication | +| `CERTCTL_GLOBALSIGN_API_SECRET` | Yes | — | API secret for request authentication | +| `CERTCTL_GLOBALSIGN_CLIENT_CERT_PATH` | Yes | — | Path to mTLS client certificate PEM | +| `CERTCTL_GLOBALSIGN_CLIENT_KEY_PATH` | Yes | — | Path to mTLS client private key PEM | +| `CERTCTL_GLOBALSIGN_SERVER_CA_PATH` | No | system trust store | PEM bundle used to verify the Atlas API server certificate. Set this for private/lab Atlas deployments whose server TLS chain is not in the host's default trust bundle. | +| `CERTCTL_GLOBALSIGN_POLL_MAX_WAIT_SECONDS` | No | `600` (10m) | Bounded-polling deadline for `GetOrderStatus`. GlobalSign tracks orders by serial number rather than order ID; the polling shape is identical. | + +## Authentication + +Dual — mTLS client certificate for TLS handshake plus `X-API-Key` +and `X-API-Secret` headers on every request. Both must be valid +or the request fails. + +## TLS verification + +The connector always verifies the server certificate. When +`server_ca_path` is set, the PEM bundle at that path is used as +the trust anchor; otherwise the host's system trust store is +used. TLS 1.2 is the minimum protocol version. + +## Issuance model + +`POST /v2/certificates` returns a serial number. Certificate PEM +is available after validation completes. Typically resolves +within seconds for DV. `GetOrderStatus` polls the certificate +endpoint. + +## mTLS keypair caching (audit fix #10) + +The parsed client certificate plus a precomputed `*http.Transport` +(with `ServerCAPath` pinning preserved when configured) are cached +on the connector after the first API call. Steady-state calls +reuse the cached transport — no per-call disk read or +`tls.X509KeyPair` parse. + +Rotation is picked up automatically via mtime polling: when the +cert file's mtime advances beyond the last-loaded value, the next +API call re-parses and rebuilds the transport. + +Operator workflow: `mv -f new.crt /etc/certctl/globalsign/client.crt` +(mtime changes), no process restart required, takes effect on the +next API call. `os.Stat` errors during rotation surface as +connector errors rather than silently serving stale credentials. + +## Revocation + +CRL and OCSP are managed by GlobalSign. certctl records +revocations locally and notifies GlobalSign via +`PUT /v2/certificates/{serial}/revoke`. + +## Operator playbook + +### Rotating mTLS client material + +Same flow as the [Entrust connector](entrust.md): place the new +cert at the configured path, mtime changes, next API call picks +up the new keypair. `ServerCAPath` pin (when configured) is +preserved across the rebuild. + +### Rotating API key / secret + +Rotate in the Atlas dashboard, then either restart certctl-server +or hot-swap via `PUT /api/v1/issuers/{id}`. The registry's +Rebuild path replaces the connector with the new credentials. The +mTLS transport cache stays warm across the swap (mTLS material +hasn't changed) — only the per-request headers are new. + +### Region selection + +Atlas HVCA has region-specific base URLs. Use the URL that +matches your account's contracted region; the connector does no +region-routing on its own. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [Entrust connector](entrust.md) — mTLS-only commercial alternative +- [DigiCert connector](digicert.md) — API-key-only commercial alternative +- [Async CA polling](../protocols/async-ca-polling.md) — the bounded-polling primitive diff --git a/docs/reference/connectors/google-cas.md b/docs/reference/connectors/google-cas.md new file mode 100644 index 0000000..854a743 --- /dev/null +++ b/docs/reference/connectors/google-cas.md @@ -0,0 +1,89 @@ +# Google CAS Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the Google Cloud Certificate +> Authority Service (CAS) issuer connector. For the +> connector-development context (interface contract, registry, +> ports/adapters), see the [connector index](index.md). + +## Overview + +Google Cloud Certificate Authority Service is a managed private CA +on GCP. Issuance is synchronous via the CAS REST API with OAuth2 +service-account auth. + +Implementation lives at `internal/connector/issuer/googlecas/`. + +## When to use this connector + +Use the Google CAS connector when: + +- Your workloads are GCP-native and you want the CA to live inside + your GCP project (for blast radius, IAM, and audit reasons). +- You want IAM-bound service-account auth instead of API keys to + rotate. +- You need GCP-native CRL distribution and audit logging served by + Google. + +Look elsewhere when: + +- You're not on GCP — AWS ACM Private CA or Azure Key Vault are + the cloud-native equivalents on those platforms. +- You need public-trust certificates — CAS is private only. +- You don't already pay for CAS (it has a non-trivial monthly + cost). Vault, step-ca, or the Local CA issuer are free + self-hosted alternatives. + +## Configuration + +| Setting | Required | Default | Description | +|---|---|---|---| +| `CERTCTL_GOOGLE_CAS_PROJECT` | Yes | — | GCP project ID | +| `CERTCTL_GOOGLE_CAS_LOCATION` | Yes | — | GCP region (e.g. `us-central1`) | +| `CERTCTL_GOOGLE_CAS_CA_POOL` | Yes | — | CA pool name | +| `CERTCTL_GOOGLE_CAS_CREDENTIALS` | Yes | — | Path to service account JSON | +| `CERTCTL_GOOGLE_CAS_TTL` | No | `8760h` | Default certificate TTL | + +## Authentication + +OAuth2 service account. The connector reads a service account +JSON file, signs a JWT with the private key, and exchanges it for +an access token at Google's token endpoint. Tokens are cached and +refreshed automatically (5 min before expiry) so the connector +doesn't pay token-mint latency on every request. + +## Revocation + +CRL and OCSP are managed by Google CAS directly. certctl records +revocations locally and notifies Google CAS via the revoke +endpoint. CAS's CRL distribution and audit logging serve the +resulting status to verifying clients. + +## Operator playbook + +### Service-account key rotation + +1. Generate a new service-account key in the GCP IAM console. +2. Distribute the new JSON to the certctl host at the + `CERTCTL_GOOGLE_CAS_CREDENTIALS` path (overwrite or use a new + path). +3. Either restart certctl-server with the new env var or hot-swap + via `PUT /api/v1/issuers/{id}` so the registry's Rebuild path + replaces the connector. +4. Delete the old key in GCP IAM after the next successful + issuance proves the new key works. + +### Required IAM roles + +The service account needs `roles/privateca.certificateRequester` +(or a custom role with `privateca.certificates.create` and +`privateca.certificates.get`) on the CA pool. Add +`roles/privateca.certificateAuthorityUser` if the connector also +needs to read the issuing CA cert chain. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [AWS ACM PCA](aws-acm-pca.md) — AWS equivalent +- [Async CA polling](../protocols/async-ca-polling.md) — bounded-polling primitive (Google CAS is synchronous so doesn't consume it) diff --git a/docs/reference/connectors/index.md b/docs/reference/connectors/index.md index ea76375..1d705f6 100644 --- a/docs/reference/connectors/index.md +++ b/docs/reference/connectors/index.md @@ -16,10 +16,18 @@ Connectors extend certctl to integrate with external systems for certificate iss Issuer connectors: +- [ACME](acme.md) — RFC 8555 v2 client (Let's Encrypt, ZeroSSL, Sectigo, Buypass, GTS, SSL.com) - [ADCS integration](adcs.md) — Active Directory Certificate Services as enterprise root via Local CA sub-CA mode - [AWS ACM Private CA](aws-acm-pca.md) — managed private CA on AWS, IAM-authenticated - [DigiCert CertCentral](digicert.md) — commercial public CA (DV / OV / EV) - [EJBCA (Keyfactor)](ejbca.md) — self-hosted open-source / Keyfactor enterprise CA +- [Entrust Certificate Services](entrust.md) — Entrust CA Gateway with mTLS auth +- [GlobalSign Atlas HVCA](globalsign.md) — Atlas HVCA with dual mTLS + API key/secret auth +- [Google CAS](google-cas.md) — managed private CA on GCP, OAuth2 service-account auth +- [Local CA](local-ca.md) — Go `crypto/x509`-backed signer (self-signed, sub-CA, tree mode) +- [OpenSSL / Custom CA](openssl.md) — script-based shell-out for arbitrary CLI-driven CAs +- [Sectigo SCM](sectigo.md) — Sectigo Certificate Manager REST API +- [step-ca (Smallstep)](step-ca.md) — JWK-provisioner authenticated synchronous internal CA - [Vault PKI](vault.md) — HashiCorp Vault PKI engine, synchronous issuance Target connectors: diff --git a/docs/reference/connectors/local-ca.md b/docs/reference/connectors/local-ca.md new file mode 100644 index 0000000..8bc7ec1 --- /dev/null +++ b/docs/reference/connectors/local-ca.md @@ -0,0 +1,170 @@ +# Local CA Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the Local CA issuer. For the +> connector-development context (interface contract, registry, +> ports/adapters), see the [connector index](index.md). + +## Overview + +The Local CA issuer signs certificates using Go's `crypto/x509` +library directly inside certctl-server. There is no external CA +service involved — certctl owns the signing key and emits +certificates synchronously. + +Implementation lives at `internal/connector/issuer/local/`. + +## When to use this connector + +Use the Local CA when: + +- You're standing up an internal-only PKI and don't want to operate + a separate CA service (Vault, step-ca, EJBCA). +- You want certctl to be the single point of administration: + signing key, profile policy, CRL and OCSP responder, and + lifecycle automation all live in one process. +- You want sub-CA mode to chain into an enterprise root (ADCS, + HSM-backed root, or another upstream CA) so existing trust + stores validate certctl-issued leaves automatically. + +Look elsewhere when: + +- You need a public-trust certificate — the Local CA is internal + only. Use ACME or DigiCert / Sectigo for public trust. +- You want signing material backed by an HSM or cloud KMS — that + is on the roadmap (the `internal/crypto/signer/` driver + abstraction exists; HSM, cloud KMS, and SSH-CA drivers don't + yet ship). Until those drivers ship, sub-CA mode pointing at a + hardware-protected root is the closest production posture. + +## Modes + +### Self-signed mode (default) + +Creates a CA on first use (in memory), issues certificates with +proper serial numbers, validity periods, SANs, and key usage +extensions. Designed for development and demos — certificates are +self-signed and not trusted by browsers without operator-side +trust-store work. + +### Sub-CA mode (production) + +Loads a CA certificate and private key from disk +(`CERTCTL_CA_CERT_PATH` + `CERTCTL_CA_KEY_PATH`). The CA cert was +signed by an upstream CA (e.g. ADCS), so all issued certificates +chain to the enterprise root trust hierarchy. Clients that +already trust the enterprise root automatically trust +certctl-issued certs. + +Supports RSA, ECDSA, and PKCS#8 key formats. If the paths are not +set, the connector falls back to self-signed mode. The loaded +certificate must have `IsCA=true` and `KeyUsageCertSign`. + +### Tree mode (Rank 8 — multi-level CA hierarchy) + +When `Issuer.HierarchyMode = "tree"` is set on the issuer row, the +connector reads the active CA hierarchy from the +`intermediate_cas` table and assembles `IssuanceResult.ChainPEM` +by walking the `parent_ca_id` ancestry from the issuing leaf CA up +to the root. + +Tree mode is operator-managed via the admin-gated +`/api/v1/issuers/{id}/intermediates` and +`/api/v1/intermediates/{id}` endpoints (`POST` to create / sign +children, `GET` to list / inspect, `POST .../retire` to two-phase +retire). The signing path is shared with single-mode (cert is +signed via `c.caCert` + `c.caSigner` from the on-disk issuing CA +cert+key); only the chain bytes differ. + +RFC 5280 §3.2 (self-signed root validation), §4.2.1.9 (path-length +tightening), and §4.2.1.10 (NameConstraints subset semantics) are +enforced at the service layer fail-closed. The default is +`single`, byte-identical to the pre-Rank-8 historical flow. + +See [intermediate-ca-hierarchy.md](../intermediate-ca-hierarchy.md) +for the operator runbook covering 4-level FedRAMP boundary CA, +3-level financial-services policy CA, 2-level internal-PKI +patterns, and the migration runbook for flipping a single-mode +issuer to tree. + +## Configuration + +```json +{ + "ca_common_name": "CertCtl Local CA", + "validity_days": 90, + "ca_cert_path": "/etc/certctl/ca/ca.pem", + "ca_key_path": "/etc/certctl/ca/ca-key.pem" +} +``` + +## CRL and OCSP (M15b) + +The Local CA serves DER-encoded X.509 CRLs unauthenticated at +`GET /.well-known/pki/crl/{issuer_id}` (RFC 5280 §5, RFC 8615, +`Content-Type: application/pkix-crl`) with 24-hour validity. + +An embedded OCSP responder at +`GET /.well-known/pki/ocsp/{issuer_id}/{serial}` (RFC 6960, +`Content-Type: application/ocsp-response`) returns signed OCSP +responses for issued certificates (good / revoked / unknown +status). + +Both endpoints are reachable by relying parties with no certctl +API credentials, which is how standard TLS clients, browsers, and +hardware appliances consume these resources. + +Certificates with profile TTL < 1 hour automatically skip +CRL/OCSP — expiry is treated as sufficient revocation for +short-lived credentials. + +## Extended Key Usage support (M27) + +The Local CA respects EKU constraints from certificate profiles +and adjusts key usage flags accordingly: + +- **S/MIME** (`emailProtection` EKU) → + `DigitalSignature | ContentCommitment`. +- **TLS** (`serverAuth` / `clientAuth` EKU) → + `DigitalSignature | KeyEncipherment`. + +This enables a single CA to issue TLS, S/MIME, code signing, and +timestamping certificates from one issuer row. + +## MaxTTL enforcement (M11c) + +When a certificate profile defines a maximum TTL, the Local CA +caps the `NotAfter` field to `min(validity_days, maxTTL)`. This +ensures certificates never exceed the profile's configured +lifetime regardless of the issuer's `validity_days` setting. + +## L-014 file-on-disk threat-model carve-out + +In file-driver mode (the default), the CA private key sits on the +certctl-server filesystem as a PEM at `CERTCTL_CA_KEY_PATH`. This +is a standard internal-PKI posture but means filesystem +compromise of the certctl host equals signing-key compromise. +Mitigations: + +- **Filesystem permissions.** Mode 0600, owned by the certctl + service user. The connector preflight refuses to load a key + whose mode is wider than 0600. +- **Sub-CA rotation.** Rotate the certctl sub-CA cert+key + periodically (yearly is a sensible default) so a captured key + has a bounded blast-radius window. +- **Filesystem audit.** Add an `auditctl` watch on the key path; + any read/write attempt outside certctl-server's process is + logged. +- **Move to alternate signer drivers when they ship.** The + `internal/crypto/signer/` interface is the integration seam; + HSM (PKCS#11), cloud KMS, and SSH-CA drivers will close the + filesystem-residency leg without changing the rest of the + signing path. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [ADCS integration](adcs.md) — sub-CA mode rooted at ADCS +- [Intermediate CA hierarchy](../intermediate-ca-hierarchy.md) — tree mode operator runbook +- [CRL and OCSP](../protocols/crl-ocsp.md) — RFC 5280 / RFC 6960 endpoint reference diff --git a/docs/reference/connectors/openssl.md b/docs/reference/connectors/openssl.md new file mode 100644 index 0000000..854573d --- /dev/null +++ b/docs/reference/connectors/openssl.md @@ -0,0 +1,157 @@ +# OpenSSL / Custom CA Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the script-based OpenSSL / +> Custom CA issuer connector. For the connector-development context +> (interface contract, registry, ports/adapters), see the +> [connector index](index.md). + +## Overview + +Script-based issuer connector for organizations with existing CA +tooling. Delegates certificate signing, revocation, and CRL +generation to user-provided shell scripts. The connector `exec`s +the script for every certificate lifecycle operation; the script +runs as the certctl-server user with that user's full filesystem +and network access. + +This is the highest-flexibility, highest-trust connector in +certctl. It exists to integrate with arbitrary CLI-driven CAs that +don't have a Go SDK — at the cost of a wider attack surface than +any other issuer. + +Implementation lives at `internal/connector/issuer/openssl/`. + +## When to use this connector + +Use the OpenSSL / Custom CA connector when: + +- Your CA is a CLI tool (BoringSSL, custom OpenSSL wrapper, + hardware-CA controller, internal CA with no published SDK) and + no Go-native adapter exists. +- You're prepared to operate the script with the same care as any + privileged binary on the host (review every line, lock the path + ownership and mode, audit invocations). + +Look elsewhere when: + +- A Go-native adapter exists for your CA (Vault, DigiCert, + Sectigo, ACME, AWS ACM PCA, Google CAS, EJBCA, Entrust, + GlobalSign, step-ca). Use the native adapter — narrower attack + surface, no shell-out exposure. +- You're in a compliance environment (PCI-DSS Level 1, FedRAMP + High, HIPAA-regulated PHI handling) where shell-out attack + surfaces are formally disallowed. +- You're running multi-tenant certctl-server where tenant-A's + script can affect tenant-B's certificates. + +## Configuration + +| Variable | Required | Description | +|---|---|---| +| `CERTCTL_OPENSSL_SIGN_SCRIPT` | Yes | Script that receives CSR on stdin and outputs signed PEM cert on stdout | +| `CERTCTL_OPENSSL_REVOKE_SCRIPT` | No | Script to revoke a certificate (receives serial number as argument) | +| `CERTCTL_OPENSSL_CRL_SCRIPT` | No | Script that outputs DER-encoded CRL on stdout | +| `CERTCTL_OPENSSL_TIMEOUT_SECONDS` | No | Script execution timeout (default 30s) | + +The sign script receives the CSR PEM on stdin and outputs the +signed certificate PEM on stdout. The connector parses the +certificate to extract serial number, validity dates, and chain +information. + +Before shell execution, serial numbers are validated as hex-only +(`^[0-9a-fA-F]+$`) and revocation reason codes are validated +against the RFC 5280 specification to prevent argv injection. Both +checks live in `internal/validation/command.go`. + +## Threat model + +certctl's OpenSSL adapter is a deliberate trade between +flexibility and attack surface. Top-10 fix #6 of the 2026-05-03 +issuer-coverage audit captured the threat model in detail; the +short version is below. + +### What the adapter accepts + +- A trusted operator pointing at a trusted script that lives in a + trusted filesystem location (`/usr/local/bin/`, + `/opt//bin/`, etc.) with appropriate ownership + (root-owned, mode 0755) and a clear audit trail + (filesystem-monitored, version-controlled). +- Env-var inheritance from the certctl-server process. Operators + must NOT export sensitive credentials (Vault tokens, API keys + for OTHER systems) into certctl-server's environment — or, if + they must, must accept that those credentials are visible to the + issuance script. The connector does not whitelist or strip env + vars before fork. +- The hex-only serial-number filter and the RFC 5280 reason-code + allow-list as defenses against argv injection. They are NOT + defenses against a malicious script. + +### What the adapter does NOT accept + +- A script path under operator-writable filesystem (`/tmp`, + `/var/tmp`, `~`) where a non-root user can swap the binary + mid-flight. **Symlink attack:** a non-root user with write + access to the directory replaces the script with a symlink to + `/etc/shadow` or `/root/.ssh/authorized_keys`; certctl-server + reads (or in the worst case writes via a malicious script) + those files. +- Untrusted script content. The script can do anything the + certctl-server user can — modify state outside `/etc/certctl/`, + exfiltrate data, write SSH keys to enable persistence. + Operators MUST review every script line before deploying. +- A multi-tenant host where multiple operators deploy scripts + under the same certctl-server. Process-level isolation isn't + enforced; one operator's script can read another's working + files (the temp CSR/cert files the connector writes to + `os.TempDir()` are mode 0600 but are visible by name to anyone + who can list the directory). + +## Mitigations operators can layer on + +- **Run certctl-server under a dedicated unprivileged user** + (e.g. `certctl:certctl`). The systemd unit ships with + `User=certctl` by default — keep it that way. +- **Pin the script path to a root-owned mode-0755 binary** + (`/usr/local/bin/issue-cert.sh`, root:root, 0755). Add a + filesystem audit rule (`auditctl -w /usr/local/bin/issue-cert.sh + -p wa -k certctl-script`) so any write attempt to the script is + logged. +- **Set a per-call timeout via `CERTCTL_OPENSSL_TIMEOUT_SECONDS`** + (default 30s). The connector wires this through + `exec.CommandContext` so a hung script is killed at the + wall-clock budget. Production operators should set it to the + upper bound of legitimate issuance time — anything longer is a + runaway. +- **Sanitise the certctl-server environment.** systemd's + `Environment=` directive lets operators allow-list which env + vars certctl-server (and therefore the script) sees. + Default-deny is the safe posture; the connector itself does NOT + scrub envs before fork. +- **Use a chroot or container.** systemd's `RootDirectory=` or + running certctl-server in a container limits the filesystem the + script can touch. +- **Audit the script's behaviour.** A wrapper script that logs + every invocation's argv + env-snapshot + exit code to a + separate audit log gives operators a forensic trail. +- **Per-call concurrency bound.** The renewal scheduler's + `CERTCTL_RENEWAL_CONCURRENCY` (Bundle L closure) bounds + scheduled traffic; ad-hoc `POST /api/v1/certificates` traffic + isn't bounded. For high-volume environments, layer a + reverse-proxy rate limit (NGINX, HAProxy) in front of the API. + +## V3-Pro forward path + +The hardened OpenSSL adapter (chroot/container by default, +env-var allow-list at the adapter layer, signed-script-binary +verification, audit-log-on-every-invocation, per-call concurrency +bound shared with the API surface) is V3-Pro work. Tracking: +`cowork/WORKSPACE-ROADMAP.md` (search "OpenSSL hardened mode"). + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [Local CA issuer](local-ca.md) — Go-native alternative when the CA can be run as a sub-CA under certctl +- [Vault PKI](vault.md), [EJBCA](ejbca.md), [DigiCert](digicert.md) — Go-native alternatives for common CA stacks diff --git a/docs/reference/connectors/sectigo.md b/docs/reference/connectors/sectigo.md new file mode 100644 index 0000000..3d47ea4 --- /dev/null +++ b/docs/reference/connectors/sectigo.md @@ -0,0 +1,98 @@ +# Sectigo SCM Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the Sectigo Certificate Manager +> (SCM) issuer connector. For the connector-development context +> (interface contract, registry, ports/adapters), see the +> [connector index](index.md). + +## Overview + +The Sectigo connector integrates with Sectigo Certificate Manager's +REST API for ordering and managing DV, OV, and EV certificates. +Like DigiCert, it uses an async order model: submit an enrollment, +receive an `sslId`, then poll for completion. + +Implementation lives at `internal/connector/issuer/sectigo/`. + +## When to use this connector + +Use the Sectigo SCM connector when: + +- You're already a Sectigo Certificate Manager customer (formerly + Comodo CA / SecureTrust SCM). +- You need OV / EV certificates that Sectigo validates before + issuance. +- You want certctl to drive renewal lifecycle on top of Sectigo's + commercial issuance. + +Look elsewhere when: + +- You're using Sectigo through their ACME endpoint — the + [ACME connector](acme.md) is a simpler path. +- You only need DV certificates and want a free public-trust CA — + Let's Encrypt or ZeroSSL via the ACME connector. + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `CERTCTL_SECTIGO_CUSTOMER_URI` | — | Sectigo customer URI (organization identifier) | +| `CERTCTL_SECTIGO_LOGIN` | — | API account login | +| `CERTCTL_SECTIGO_PASSWORD` | — | API account password | +| `CERTCTL_SECTIGO_ORG_ID` | — | Organization ID (integer) | +| `CERTCTL_SECTIGO_CERT_TYPE` | — | Certificate type ID (integer, from `/ssl/v1/types`) | +| `CERTCTL_SECTIGO_TERM` | `365` | Certificate validity in days | +| `CERTCTL_SECTIGO_BASE_URL` | `https://cert-manager.com/api` | Sectigo API base URL | +| `CERTCTL_SECTIGO_POLL_MAX_WAIT_SECONDS` | `600` | Bounded-polling deadline for `GetOrderStatus` | + +## Authentication + +Three custom headers on every request: `customerUri`, `login`, +and `password`. No mTLS or OAuth2. + +## Issuance model + +`POST /ssl/v1/enroll` returns an `sslId`. DV certificates may +issue immediately; OV/EV certificates require Sectigo-side +validation and poll-based completion. + +`GetOrderStatus` runs bounded internal polling +(5s/15s/45s/2m/5m capped, ±20% jitter, default 10-minute +deadline). The `collectNotReady` sentinel (cert approved but not +yet retrievable) rides the same backoff schedule. Bump +`CERTCTL_SECTIGO_POLL_MAX_WAIT_SECONDS` for OV/EV workflows where +human approval extends past 10 minutes — see +[async-ca-polling.md](../protocols/async-ca-polling.md) for the +schedule shape and tuning guidance. + +## Revocation + +CRL and OCSP are managed by Sectigo. certctl records revocations +locally and notifies Sectigo via `/ssl/v1/revoke/{sslId}`. Unlike +DigiCert (no auto-notify), Sectigo's revocation is part of the +connector's revoke path. + +## Operator playbook + +### Credential rotation + +Rotate the API password in Sectigo's admin portal, then either +restart certctl-server with the new value in +`CERTCTL_SECTIGO_PASSWORD` or hot-swap via `PUT /api/v1/issuers/{id}`. +The registry's Rebuild path replaces the connector with the new +credentials. No certificate state is invalidated. + +### Diagnosing slow OV/EV issuance + +Sectigo's OV/EV vetting is human-driven and can take hours to +days. The same operational pattern as DigiCert applies: issue OV/EV +certs well ahead of expiry so the bounded poll deadline is short. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [Async CA polling](../protocols/async-ca-polling.md) — the bounded-polling primitive +- [DigiCert connector](digicert.md) — comparable commercial CA alternative +- [ACME connector](acme.md) — simpler path when Sectigo is reachable via ACME diff --git a/docs/reference/connectors/step-ca.md b/docs/reference/connectors/step-ca.md new file mode 100644 index 0000000..44aa4c9 --- /dev/null +++ b/docs/reference/connectors/step-ca.md @@ -0,0 +1,99 @@ +# step-ca (Smallstep) Issuer Connector — Operator Deep-Dive + +> Last reviewed: 2026-05-05 +> +> Operator-grade documentation for the step-ca issuer connector. +> For the connector-development context (interface contract, +> registry, ports/adapters), see the [connector index](index.md). + +## Overview + +The step-ca connector integrates with Smallstep's step-ca private +CA using its native `/sign` API with JWK provisioner +authentication. Issuance is synchronous — submit a CSR plus a +provisioner-signed token, get back a signed certificate in the +same response. + +This is simpler than ACME for internal PKI: no challenge solving, +no domain validation, just CSR + auth token → signed certificate. +For ACME-based step-ca usage, point the ACME connector at +step-ca's ACME directory URL instead. + +Implementation lives at `internal/connector/issuer/stepca/`. + +## When to use this connector + +Use the step-ca connector when: + +- You already run step-ca as your internal CA and want certctl to + drive lifecycle automation on top. +- You want synchronous issuance against an internal CA without + ACME's challenge dance. +- You want certctl to enforce profile / MaxTTL policy on step-ca- + issued certs. + +Look elsewhere when: + +- You want to use step-ca's ACME directory — that path goes + through the [ACME connector](acme.md) instead, which gives you + ACME features (ARI, EAB, profile selection) on top. +- You don't already run step-ca and want a simpler internal CA — + the [Local CA](local-ca.md) issuer is a one-process alternative. + +## Configuration + +```json +{ + "ca_url": "https://ca.internal:9000", + "provisioner_name": "certctl", + "provisioner_key_path": "/etc/certctl/stepca/provisioner.json", + "provisioner_password": "...", + "root_cert_path": "/etc/certctl/stepca/root_ca.crt", + "validity_days": 90 +} +``` + +Environment variables: + +- `CERTCTL_STEPCA_URL` — step-ca server URL +- `CERTCTL_STEPCA_PROVISIONER` — JWK provisioner name +- `CERTCTL_STEPCA_KEY_PATH` — Path to provisioner private key + (JWK JSON) +- `CERTCTL_STEPCA_PASSWORD` — Provisioner key password + +## Authentication: JWK provisioner + +A JWK provisioner is created in step-ca with a passphrase-encrypted +private key (JSON Web Key format). certctl signs short-lived +proof-of-authorization tokens with the provisioner key for each +issuance request. The provisioner password is needed to decrypt the +JWK on disk; it is held in memory by certctl-server. + +Rotation: rotate the JWK provisioner in step-ca, distribute the new +JWK + password to certctl, then either restart certctl-server or +hot-swap via `PUT /api/v1/issuers/{id}` so the registry's Rebuild +path replaces the connector with the new provisioner config. + +## MaxTTL enforcement (M11c) + +When a certificate profile defines a maximum TTL, the step-ca +connector caps the `NotAfter` field to ensure the issued +certificate does not exceed the profile limit, regardless of the +step-ca provisioner's own maximum. + +## Revocation and CRL/OCSP + +step-ca-issued certificates rely on step-ca's own CRL/OCSP +infrastructure. certctl's local CRL/OCSP endpoints +(`GET /.well-known/pki/crl/{issuer_id}` and +`GET /.well-known/pki/ocsp/{issuer_id}/{serial}`, served +unauthenticated per RFC 5280 §5 / RFC 6960 / RFC 8615) are +populated from step-ca's revocation data if available, but clients +should validate against step-ca's endpoints for the authoritative +status. + +## Related docs + +- [Connector index](index.md) — interface contract, registry, port/adapter wiring +- [ACME connector](acme.md) — alternative path to step-ca via its ACME directory URL +- [Local CA issuer](local-ca.md) — simpler internal-CA alternative when step-ca isn't already deployed