mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 21:31:34 +00:00
docs: Phase 4 follow-on batch 2 — 8 remaining issuer per-pages
Extracts the rest of the issuer per-connector deep-dive pages: - local-ca.md (170 lines) — Local CA self-signed / sub-CA / tree mode, CRL+OCSP endpoints, EKU support, MaxTTL enforcement, L-014 file-on- disk threat model carve-out - acme.md (235 lines) — RFC 8555 v2 client (HTTP-01 / DNS-01 / DNS-PERSIST-01), ARI per RFC 9773, EAB + ZeroSSL auto-EAB, Let's Encrypt profile selection, revoke-by-serial Top-10 fix #7 - step-ca.md (99 lines) — Smallstep JWK-provisioner synchronous issuance with MaxTTL enforcement - openssl.md (157 lines) — script-based shell-out with full threat model (what's accepted, what's not, mitigations, V3-Pro forward path) - sectigo.md (98 lines) — Sectigo SCM REST with bounded async polling - google-cas.md (89 lines) — GCP managed private CA with OAuth2 service-account auth + IAM-role guidance - entrust.md (96 lines) — Entrust CA Gateway mTLS-authenticated with approval-pending support and mTLS keypair caching - globalsign.md (122 lines) — Atlas HVCA dual auth (mTLS + API key/secret), region-aware base URLs, mTLS keypair caching Index forward-list expanded to enumerate all 13 issuer connectors (including the 5 pages from batch 1) in alphabetical order. This is part 2 of 4 for the Phase 4 follow-on (per-connector page extraction) tracked in cowork/docs-overhaul-phase-2-restructure-2026-05-04/log.md. Net add: 8 files, 1,066 lines. No content removed from index.md.
This commit is contained in:
@@ -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.<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
|
||||||
|
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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -16,10 +16,18 @@ Connectors extend certctl to integrate with external systems for certificate iss
|
|||||||
|
|
||||||
Issuer connectors:
|
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
|
- [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
|
- [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)
|
- [DigiCert CertCentral](digicert.md) — commercial public CA (DV / OV / EV)
|
||||||
- [EJBCA (Keyfactor)](ejbca.md) — self-hosted open-source / Keyfactor enterprise CA
|
- [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
|
- [Vault PKI](vault.md) — HashiCorp Vault PKI engine, synchronous issuance
|
||||||
|
|
||||||
Target connectors:
|
Target connectors:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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/<vendor>/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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user