From e9b15108d9cdc6da8fab03ee96a04486e02a325d Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Tue, 5 May 2026 02:55:45 +0000 Subject: [PATCH] docs: split legacy-est-scep.md into two purpose-aligned docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 519-line legacy-est-scep.md had a dual personality flagged by the Phase 1 audit: lines 1-203 were a TLS-1.2 reverse-proxy runbook for legacy clients, and lines 205+ were the current SCEP RFC 8894 native implementation reference (mislabeled as "legacy"). Two separate audiences, two separate purposes. Split: Lines 1-203 (TLS-1.2 reverse-proxy runbook): → docs/operator/legacy-clients-tls-1.2.md (NEW) Operator runbook for the case where embedded EST/SCEP clients only speak TLS 1.2. Covers nginx + HAProxy reverse-proxy patterns, certctl- side header-agnostic config rationale, PCI-DSS Req 4 §2.2.5 attestation, deprecation timeline. Also got a fresh "What this is" framing. Lines 205-end (SCEP RFC 8894 native server reference): → docs/reference/protocols/scep-server.md (NEW) Generic SCEP server protocol reference: RA cert + key configuration, GetCACaps capability advertisement, supported messageTypes, MVP backward-compat path, multi-profile dispatch, must-staple per-profile policy, mTLS sibling route, Microsoft Intune dynamic-challenge dispatcher. Cross-links to scep-intune.md for Intune-specific deployment guidance. Both new docs carry a `Last reviewed: 2026-05-05` line. Internal links within each new doc updated to the new sibling paths. Cross-references from other docs to legacy-est-scep.md still need fixing in Phase 11. Original docs/legacy-est-scep.md deleted (git history preserves). --- docs/operator/legacy-clients-tls-1.2.md | 215 ++++++++++++++ .../protocols/scep-server.md} | 275 +++--------------- 2 files changed, 260 insertions(+), 230 deletions(-) create mode 100644 docs/operator/legacy-clients-tls-1.2.md rename docs/{legacy-est-scep.md => reference/protocols/scep-server.md} (56%) diff --git a/docs/operator/legacy-clients-tls-1.2.md b/docs/operator/legacy-clients-tls-1.2.md new file mode 100644 index 0000000..dd7828a --- /dev/null +++ b/docs/operator/legacy-clients-tls-1.2.md @@ -0,0 +1,215 @@ +# Legacy Clients (TLS 1.2) — Reverse-Proxy Runbook + +> Last reviewed: 2026-05-05 + +**Audit reference:** Bundle F / M-023. PCI-DSS v4.0 Req 4 §2.2.5; CWE-326. + +## 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, the SOC 2 / PCI-DSS / NIST SP 800-57 compliance +mappings, and the M-001 PBKDF2 work factor all 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 + +```mermaid +flowchart LR + Client["legacy EST/SCEP client"] + Proxy["nginx / HAProxy
reverse proxy"] + Server["certctl :8443"] + Client -->|"TLS 1.2/1.3
(allowed TLS 1.2)"| Proxy + Proxy -->|"TLS 1.3
(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 + +```nginx +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 the strong AEAD suites that + # PCI-DSS Req 4 §2.2.5 still allows under TLS 1.2. + 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. + +## PCI-DSS Req 4 §2.2.5 attestation + +PCI-DSS v4.0 §2.2.5 ("strong cryptography for authentication/transmission +of cardholder data") considers TLS 1.2 with strong cipher suites +acceptable for the foreseeable future, with the explicit caveat that NIST +or the PCI Council may shorten the deprecation window if a TLS 1.2 +weakness is published. 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. + +This is PCI-DSS Req 4 v4.0 compliant. Auditors 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 + +PCI-DSS, NIST, and major browsers will eventually deprecate TLS 1.2. +When that happens, this runbook becomes obsolete; the only path forward +will be to replace the legacy clients. Subscribe to RSS feeds at the +following sources to catch the deprecation announcement before it +becomes a compliance failure: + +- https://www.pcisecuritystandards.org/news_events/ +- https://nvlpubs.nist.gov/nistpubs/SpecialPublications/ (SP 800-52 revisions) + +## Related docs + +- [`docs/operator/tls.md`](tls.md) — the certctl-internal TLS configuration (HTTPS-only control plane, MinVersion pin) +- [`docs/operator/security.md`](security.md) — overall security posture +- [`docs/operator/database-tls.md`](database-tls.md) — Postgres TLS opt-in (Bundle B / M-018) +- [`docs/reference/protocols/scep-server.md`](../reference/protocols/scep-server.md) — SCEP RFC 8894 native server reference +- [`docs/reference/protocols/est.md`](../reference/protocols/est.md) — EST RFC 7030 server reference diff --git a/docs/legacy-est-scep.md b/docs/reference/protocols/scep-server.md similarity index 56% rename from docs/legacy-est-scep.md rename to docs/reference/protocols/scep-server.md index 7de95d7..5383f16 100644 --- a/docs/legacy-est-scep.md +++ b/docs/reference/protocols/scep-server.md @@ -1,222 +1,36 @@ -# Legacy EST / SCEP Clients — TLS 1.2 Reverse-Proxy Runbook +# SCEP Server (RFC 8894) — Protocol Reference -**Audit reference:** Bundle F / M-023. PCI-DSS v4.0 Req 4 §2.2.5; CWE-326. +> Last reviewed: 2026-05-05 -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. +## What this is -## Why TLS 1.3 minimum +certctl ships a native RFC 8894 SCEP server. This reference covers the +protocol surface: RA cert + key configuration, capability advertisement, +supported messageTypes, multi-profile dispatch, must-staple policy, mTLS +sibling routing, and Microsoft Intune dynamic-challenge dispatcher. -certctl's audit posture, the SOC 2 / PCI-DSS / NIST SP 800-57 compliance -mappings, and the M-001 PBKDF2 work factor all 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. +For Intune-specific deployment guidance (NDES replacement playbook, +Intune SCEP profile field mapping, troubleshooting matrix specific to +Intune deployments, Microsoft support statement), see +[`scep-intune.md`](scep-intune.md). For the legacy-client TLS 1.2 +reverse-proxy runbook, see +[`docs/operator/legacy-clients-tls-1.2.md`](../../operator/legacy-clients-tls-1.2.md). -## When this runbook applies +## How it works -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 - -```mermaid -flowchart LR - Client["legacy EST/SCEP client"] - Proxy["nginx / HAProxy
reverse proxy"] - Server["certctl :8443"] - Client -->|"TLS 1.2/1.3
(allowed TLS 1.2)"| Proxy - Proxy -->|"TLS 1.3
(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 - -```nginx -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 the strong AEAD suites that - # PCI-DSS Req 4 §2.2.5 still allows under TLS 1.2. - 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. - -## PCI-DSS Req 4 §2.2.5 attestation - -PCI-DSS v4.0 §2.2.5 ("strong cryptography for authentication/transmission -of cardholder data") considers TLS 1.2 with strong cipher suites -acceptable for the foreseeable future, with the explicit caveat that NIST -or the PCI Council may shorten the deprecation window if a TLS 1.2 -weakness is published. 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. - -This is PCI-DSS Req 4 v4.0 compliant. Auditors 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 - -PCI-DSS, NIST, and major browsers will eventually deprecate TLS 1.2. -When that happens, this runbook becomes obsolete; the only path forward -will be to replace the legacy clients. Subscribe to RSS feeds at the -following sources to catch the deprecation announcement before it -becomes a compliance failure: - -- https://www.pcisecuritystandards.org/news_events/ -- https://nvlpubs.nist.gov/nistpubs/SpecialPublications/ (SP 800-52 revisions) - -## SCEP RFC 8894 native implementation (post-2026-04-29) - -Prior to this bundle, certctl's SCEP server parsed `PKCS#7 SignedData` and -treated the encapsulated content as a raw `PKCS#10 CSR` (the file-internal -"MVP" comment at `internal/api/handler/scep.go:217` flagged this). That -worked for lightweight MDM agents but failed against ChromeOS and most -production MDM clients which expect full RFC 8894 wire format: -`SignedData` wrapping an `EnvelopedData` encrypting the CSR to the RA -cert's public key, with `signerInfo` POPO over the auth-attrs. +Prior to the RFC 8894 native implementation, certctl's SCEP server parsed +`PKCS#7 SignedData` and treated the encapsulated content as a raw +`PKCS#10 CSR` (the file-internal "MVP" path). That worked for lightweight +MDM agents but failed against ChromeOS and most production MDM clients +which expect full RFC 8894 wire format: `SignedData` wrapping an +`EnvelopedData` encrypting the CSR to the RA cert's public key, with +`signerInfo` POPO over the auth-attrs. The new RFC 8894 path runs FIRST; on any parse failure it falls through to the legacy MVP raw-CSR path so existing operators see no behavior change for their lightweight clients. -### Required: RA cert + key +## Required: RA cert + key The RFC 8894 path requires a Registration Authority cert + key pair. Clients encrypt their CSR to the RA cert's public key (RFC 8894 §3.2.2); @@ -255,7 +69,7 @@ validates: file existence, key file mode 0600, cert/key match, cert non-expired, RSA-or-ECDSA public-key algorithm. Failures `os.Exit(1)` with a structured log line identifying the offending profile. -### Capability advertisement (`GetCACaps`) +## Capability advertisement (`GetCACaps`) ``` POSTPKIOperation @@ -272,7 +86,7 @@ ChromeOS specifically looks for `POSTPKIOperation` (non-base64 POST), Older Cisco IOS clients also accept `SHA-256` and `SHA-512` per RFC 8894 §3.5.2. -### Supported messageTypes +## Supported messageTypes | Type | RFC 8894 § | Behavior | | --- | --- | --- | @@ -281,7 +95,7 @@ Older Cisco IOS clients also accept `SHA-256` and `SHA-512` per RFC 8894 | `GetCertInitial` (20) | §3.3.3 | Polling for pending requests. v1 returns `FAILURE+badCertID` because deferred-issuance isn't supported (every PKCSReq either succeeds or fails synchronously). | | `CertRep` (3) | §3.3.2 | Server response — never inbound. | -### MVP backward-compatibility path +## MVP backward-compatibility path Lightweight clients that send a stripped `SignedData` containing a raw CSR (no `EnvelopedData` wrapper, no `signerInfo` POPO) keep working: the @@ -291,14 +105,13 @@ the CSR's `challengePassword` attribute the same way as the RFC 8894 path. Operators with existing lightweight-client deploys see zero behavior change. -### Multi-profile dispatch (`/scep/`) +## Multi-profile dispatch (`/scep/`) Real enterprise deploys run multiple SCEP endpoints from one certctl instance — corp-laptop CA, IoT CA, server CA — each with its own issuer + RA pair + challenge password. Configure via the indexed env-var -form documented in [`features.md`](features.md): set -`CERTCTL_SCEP_PROFILES=corp,iot,server` (a comma-separated list of -profile names), then for each name supply the per-profile env-vars +form: set `CERTCTL_SCEP_PROFILES=corp,iot,server` (a comma-separated list +of profile names), then for each name supply the per-profile env-vars prefixed with `CERTCTL_SCEP_PROFILE__` followed by the suffix keys `_ISSUER_ID`, `_PROFILE_ID`, `_CHALLENGE_PASSWORD`, `_RA_CERT_PATH`, `_RA_KEY_PATH`. The `` token resolves to the upper-cased profile @@ -310,7 +123,7 @@ The router exposes `/scep/corp`, `/scep/iot`, `/scep/server`. The legacy `CERTCTL_SCEP_PROFILES` is unset). Per-profile preflight validates each RA pair independently; failures log the offending PathID. -### ChromeOS Admin Console pointer +## ChromeOS Admin Console pointer In Google Admin Console → Devices → Networks → Certificates, register certctl's `/scep[/]` URL as the SCEP server. Enter the challenge @@ -319,7 +132,7 @@ password from `CERTCTL_SCEP_CHALLENGE_PASSWORD` (or per-profile `GetCACert` first to retrieve the RA cert, then enrolls via PKIOperation. -### RA cert rotation +## RA cert rotation The RA cert is loaded once at startup and persisted in the handler's struct field; rotation requires a server restart (mirrors the @@ -328,7 +141,7 @@ recommended cadence is annual rotation with a 30-day overlap during which both old + new RA certs are listed in `GetCACert`'s response (set the cert chain accordingly in your sub-CA hierarchy). -### Must-staple per-profile policy (RFC 7633) +## Must-staple per-profile policy (RFC 7633) When a `CertificateProfile` has `MustStaple = true`, the local issuer adds the `id-pe-tlsfeature` extension (OID `1.3.6.1.5.5.7.1.24`, @@ -347,7 +160,7 @@ Recommended for: Intune-deployed device certs (modern TLS clients); SCEP profiles serving general / legacy clients (ChromeOS, IoT) should stay `false` until the TLS path is verified. -### mTLS sibling route (Phase 6.5, opt-in) +## mTLS sibling route (Phase 6.5, opt-in) SCEP is documented as application-layer-auth — the challenge password is the authentication boundary per RFC 8894 §3.2. But enterprise @@ -408,7 +221,7 @@ challenge+mTLS: 1. Generate a bootstrap CA + issue a bootstrap cert per device (out of band — typically manufacturing-time, MDM-pushed, or a separate - PKI flow). + PKI flow). 2. Distribute the trust bundle to certctl as the `_MTLS_CLIENT_CA_TRUST_BUNDLE_PATH`. 3. Set `_MTLS_ENABLED=true` for the profile, restart certctl. @@ -421,7 +234,7 @@ challenge+mTLS: the password requirement doesn't go away — the password is still the application-layer auth boundary). -### Microsoft Intune dynamic-challenge dispatcher (Phase 8, opt-in) +## Microsoft Intune dynamic-challenge dispatcher (Phase 8, opt-in) When SCEP sits behind the Microsoft Intune Certificate Connector, devices present an Intune-issued signed challenge (a JWT-like blob over a JSON @@ -488,7 +301,7 @@ the dispatcher routes Intune-shaped challenges (length > 200 + exactly two dots) to the validator and falls through to the static compare otherwise. -### Operational notes +## Operational notes - **Audit:** every enrollment emits an `audit_event` row with action `scep_pkcsreq` (initial) or `scep_renewalreq` (renewal); operators @@ -498,8 +311,10 @@ otherwise. bodies at `CERTCTL_MAX_BODY_SIZE` (default 1MB); SCEP PKIMessages are typically <50KB so the default cap is generous. - **HTTPS-only:** the SCEP endpoint inherits the TLS-1.3-pinned control - plane; there is no plaintext fallback. -- **For Microsoft Intune deployments, see [`scep-intune.md`](scep-intune.md)** — + plane; there is no plaintext fallback. Legacy clients that only speak + TLS 1.2 use the reverse-proxy bridge documented at + [`docs/operator/legacy-clients-tls-1.2.md`](../../operator/legacy-clients-tls-1.2.md). +- **For Microsoft Intune deployments,** see [`scep-intune.md`](scep-intune.md) — architecture, NDES-replacement migration playbook, Intune SCEP profile field mapping, trust-anchor extraction recipe, troubleshooting matrix, operational monitoring, V3-Pro deferrals, and the Microsoft support @@ -508,12 +323,12 @@ otherwise. mTLS sibling-route status, challenge-password-set indicator, and the full SCEP audit log filter), the admin GUI page lives at `/scep` with three tabs: **Profiles** (default), **Intune Monitoring**, - **Recent Activity**. See `scep-intune.md::Operational monitoring` - for the Intune-specific tab inside it. + **Recent Activity**. See the operational-monitoring section in + [`scep-intune.md`](scep-intune.md) for the Intune-specific tab. ## Related docs -- [`tls.md`](tls.md) — the certctl-internal TLS configuration (HTTPS-only - control plane, MinVersion pin) -- [`security.md`](security.md) — overall security posture -- [`database-tls.md`](database-tls.md) — Postgres TLS opt-in (Bundle B / M-018) +- [`scep-intune.md`](scep-intune.md) — Microsoft Intune deployment guide +- [`est.md`](est.md) — EST RFC 7030 server reference +- [`docs/operator/legacy-clients-tls-1.2.md`](../../operator/legacy-clients-tls-1.2.md) — TLS 1.2 reverse-proxy runbook for legacy SCEP clients +- [`docs/reference/architecture.md`](../architecture.md) — system design including SCEP server placement