Files
certctl/docs/operator/legacy-clients-tls-1.2.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

9.0 KiB

Legacy Clients (TLS 1.2) — Reverse-Proxy Runbook

Last reviewed: 2026-05-05

Audit reference: Bundle F / M-023. CWE-326 (Inadequate encryption strength).

What this is

certctl's control plane pins tls.Config.MinVersion = tls.VersionTLS13 (cmd/server/tls.go:131). Some embedded EST (RFC 7030) and SCEP (RFC 8894) clients only speak TLS 1.0/1.1/1.2 — those clients cannot complete the handshake against certctl directly. This runbook documents the supported operator pattern: terminate the legacy TLS version at a front-door reverse proxy and pass the request through to certctl over TLS 1.3.

Why TLS 1.3 minimum

certctl's audit posture and the M-001 PBKDF2 work factor both assume modern transport crypto. TLS 1.2 with the cipher suites still in the wild has known attack surface (BEAST, POODLE, ROBOT, raccoon — all CVE-categorized); allowing TLS 1.2 directly on the certctl listener would invalidate the guarantee that the server-side encryption chain is the strongest the ecosystem currently supports.

When this runbook applies

You need this if all three are true:

  1. You operate certctl with EST or SCEP enabled (CERTCTL_EST_ENABLED=true or CERTCTL_SCEP_ENABLED=true).
  2. Your enrolling clients are embedded devices (printers, network appliances, IoT boards, legacy MFPs, point-of-sale terminals) whose TLS stack pre-dates 2018 and only speaks TLS 1.2 or older.
  3. Replacing those clients is not feasible on a 6-month horizon.

If your enrolling clients are modern (any current Linux/Windows/macOS host, anything Go-based, anything Rust/Python/Node from 2019 onward), they speak TLS 1.3 natively and this runbook is unnecessary — point them straight at certctl on :8443.

Architecture

flowchart LR
    Client["legacy EST/SCEP client"]
    Proxy["nginx / HAProxy<br/>reverse proxy"]
    Server["certctl :8443"]
    Client -->|"TLS 1.2/1.3<br/>(allowed TLS 1.2)"| Proxy
    Proxy -->|"TLS 1.3<br/>(re-encrypts as TLS 1.3)"| Server

The reverse proxy:

  • Terminates the legacy-version TLS handshake on the public-facing port.
  • Forwards the request to certctl over TLS 1.3 on a private network.
  • (For EST mTLS) forwards the client certificate via an X-SSL-Client-Cert header that certctl reads only when the connection arrives from a configured-trusted source IP.

nginx config

upstream certctl_backend {
    # Private-network address; not reachable from outside the proxy host.
    server 10.0.0.10:8443;
}

server {
    listen 443 ssl http2;
    server_name est.example.com;

    # Public-facing legacy listener. ssl_protocols includes TLSv1.2 explicitly.
    # Keep ssl_ciphers conservative — only strong AEAD suites with forward
    # secrecy.
    ssl_certificate     /etc/nginx/certs/est.example.com.fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/est.example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # mTLS for EST: optional client cert, verified against the EST CA.
    ssl_client_certificate /etc/nginx/certs/est-clients-ca.pem;
    ssl_verify_client      optional;

    location ~ ^/\.well-known/(est|pki) {
        # Forward the client cert (if presented) to certctl over the
        # private hop. The current certctl implementation IGNORES the
        # X-SSL-Client-Cert header (header-agnostic by default — see
        # the certctl-side configuration section below). EST/SCEP
        # authentication still works correctly because both protocols
        # carry their own auth (CSR signature for EST, challengePassword
        # for SCEP) inside the request body.
        proxy_set_header X-SSL-Client-Cert  $ssl_client_escaped_cert;
        proxy_set_header X-Forwarded-For    $remote_addr;
        proxy_set_header X-Forwarded-Proto  $scheme;

        # The proxy-to-certctl hop is itself TLS 1.3.
        proxy_pass https://certctl_backend;
        proxy_ssl_protocols TLSv1.3;
        proxy_ssl_verify    on;
        proxy_ssl_trusted_certificate /etc/nginx/certs/certctl-internal-ca.pem;
    }

    # SCEP endpoints — same pattern, no client-cert requirement
    # (SCEP authenticates via challengePassword inside the CSR).
    location ^~ /scep {
        proxy_set_header X-Forwarded-For    $remote_addr;
        proxy_set_header X-Forwarded-Proto  $scheme;
        proxy_pass https://certctl_backend;
        proxy_ssl_protocols TLSv1.3;
        proxy_ssl_verify    on;
        proxy_ssl_trusted_certificate /etc/nginx/certs/certctl-internal-ca.pem;
    }
}

HAProxy config (alternative)

frontend est_legacy
    bind *:443 ssl crt /etc/haproxy/certs/est.example.com.pem alpn h2,http/1.1 \
        ssl-min-ver TLSv1.2 \
        ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384

    acl is_est_path  path_beg /.well-known/est
    acl is_pki_path  path_beg /.well-known/pki
    acl is_scep_path path_beg /scep
    use_backend certctl_backend if is_est_path or is_pki_path or is_scep_path
    default_backend certctl_modern

backend certctl_backend
    server certctl 10.0.0.10:8443 ssl verify required \
        ca-file /etc/haproxy/certs/certctl-internal-ca.pem \
        ssl-min-ver TLSv1.3
    http-request set-header X-Forwarded-For %[src]
    http-request set-header X-Forwarded-Proto https

certctl-side configuration

The current implementation is header-agnostic: certctl ignores any X-SSL-Client-Cert / X-Forwarded-For headers from the proxy. EST authentication still happens via in-protocol CSR signature + profile policy (RFC 7030 §3.2.3); SCEP authentication still happens via the challengePassword attribute embedded in the CSR (RFC 8894 §3.2). Both mechanisms are inside the request body and survive the reverse-proxy hop without server-side header trust.

Why this is the correct default: trusting a proxy-supplied header for client identity opens a header-spoofing attack surface that requires careful design (CIDR allowlist of trusted proxies, fail-closed defaults, explicit operator opt-in). The Bundle F closure of M-023 ships the TLS-bridge guidance as documentation only; a future commit can extend certctl with proxy-header trust if and when an operator demonstrates a deployment shape that requires it. Until that lands, the runbook above is operationally complete: legacy EST and SCEP clients continue to authenticate via their in-protocol mechanisms, and the reverse proxy is purely a TLS-version bridge.

If your deployment requires proxy-supplied client identity (e.g., the proxy terminates mTLS and you want certctl to record the client-cert subject in the audit trail beyond what the CSR carries), open an issue and a future commit will add a header-trust contract behind two fail-closed env vars: a CIDR allowlist of trusted proxies, plus an explicit opt-in toggle. Both knobs would be required together; setting only one would fail loud at startup. Until that work ships, the header-agnostic default described above is the only supported configuration.

TLS posture summary

The configuration above:

  • Pins TLS 1.2 + TLS 1.3 only (no SSLv3, TLS 1.0, TLS 1.1).
  • Uses only AEAD cipher suites with forward secrecy (ECDHE-* with GCM or ChaCha20-Poly1305).
  • Re-encrypts to TLS 1.3 on the proxy-to-certctl hop so the certctl listener never speaks anything below 1.3.

That is the strongest posture currently achievable while still allowing the legacy clients to enroll. Reviewers looking for the attestation should be pointed at this section + the proxy's TLS config.

What this runbook does NOT cover

  • Replacing the legacy clients. That's the long-term fix; this runbook is the bridge while you're migrating.
  • Network segmentation. The reverse proxy assumes the proxy-to-certctl hop is on a network that an external attacker can't reach. If it's not, you need a deeper architecture review.
  • Client-cert revocation. EST mTLS revocation is the relying party's responsibility. certctl's EST handler accepts the cert; the proxy can enforce CRL/OCSP via ssl_crl_path (nginx) or crl-file (HAProxy).

When TLS 1.2 itself sunsets

Major browsers and OS vendors will eventually deprecate TLS 1.2. When that happens, this runbook becomes obsolete; the only path forward will be to replace the legacy clients. Watch the IETF TLS working group, the major browser vendors' announcement channels, and your own embedded-device vendors for deprecation notices.