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

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

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

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

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

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

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

157 lines
6.9 KiB
Markdown

# 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 regulated environment where shell-out attack
surfaces are formally disallowed by your security policy.
- 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