docs: split legacy-est-scep.md into two purpose-aligned docs

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).
This commit is contained in:
shankar0123
2026-05-05 02:55:45 +00:00
parent f157c18368
commit e9b15108d9
2 changed files with 260 additions and 230 deletions
+215
View File
@@ -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<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
```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
@@ -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` ## What this is
(`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 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 For Intune-specific deployment guidance (NDES replacement playbook,
mappings, and the M-001 PBKDF2 work factor all assume modern transport Intune SCEP profile field mapping, troubleshooting matrix specific to
crypto. TLS 1.2 with the cipher suites still in the wild has known Intune deployments, Microsoft support statement), see
attack surface (BEAST, POODLE, ROBOT, raccoon — all CVE-categorized); [`scep-intune.md`](scep-intune.md). For the legacy-client TLS 1.2
allowing TLS 1.2 directly on the certctl listener would invalidate the reverse-proxy runbook, see
guarantee that the server-side encryption chain is the strongest the [`docs/operator/legacy-clients-tls-1.2.md`](../../operator/legacy-clients-tls-1.2.md).
ecosystem currently supports.
## When this runbook applies ## How it works
You need this if **all three** are true: Prior to the RFC 8894 native implementation, certctl's SCEP server parsed
`PKCS#7 SignedData` and treated the encapsulated content as a raw
1. You operate certctl with EST or SCEP enabled (`CERTCTL_EST_ENABLED=true` `PKCS#10 CSR` (the file-internal "MVP" path). That worked for lightweight
or `CERTCTL_SCEP_ENABLED=true`). MDM agents but failed against ChromeOS and most production MDM clients
2. Your enrolling clients are embedded devices (printers, network which expect full RFC 8894 wire format: `SignedData` wrapping an
appliances, IoT boards, legacy MFPs, point-of-sale terminals) whose TLS `EnvelopedData` encrypting the CSR to the RA cert's public key, with
stack pre-dates 2018 and only speaks TLS 1.2 or older. `signerInfo` POPO over the auth-attrs.
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<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
```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.
The new RFC 8894 path runs FIRST; on any parse failure it falls through 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 to the legacy MVP raw-CSR path so existing operators see no behavior
change for their lightweight clients. change for their lightweight clients.
### Required: RA cert + key ## Required: RA cert + key
The RFC 8894 path requires a Registration Authority cert + key pair. 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); 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)` non-expired, RSA-or-ECDSA public-key algorithm. Failures `os.Exit(1)`
with a structured log line identifying the offending profile. with a structured log line identifying the offending profile.
### Capability advertisement (`GetCACaps`) ## Capability advertisement (`GetCACaps`)
``` ```
POSTPKIOperation 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 Older Cisco IOS clients also accept `SHA-256` and `SHA-512` per RFC 8894
§3.5.2. §3.5.2.
### Supported messageTypes ## Supported messageTypes
| Type | RFC 8894 § | Behavior | | 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). | | `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. | | `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 Lightweight clients that send a stripped `SignedData` containing a raw
CSR (no `EnvelopedData` wrapper, no `signerInfo` POPO) keep working: the 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 path. Operators with existing lightweight-client deploys see zero
behavior change. behavior change.
### Multi-profile dispatch (`/scep/<pathID>`) ## Multi-profile dispatch (`/scep/<pathID>`)
Real enterprise deploys run multiple SCEP endpoints from one certctl Real enterprise deploys run multiple SCEP endpoints from one certctl
instance — corp-laptop CA, IoT CA, server CA — each with its own instance — corp-laptop CA, IoT CA, server CA — each with its own
issuer + RA pair + challenge password. Configure via the indexed env-var issuer + RA pair + challenge password. Configure via the indexed env-var
form documented in [`features.md`](features.md): set form: set `CERTCTL_SCEP_PROFILES=corp,iot,server` (a comma-separated list
`CERTCTL_SCEP_PROFILES=corp,iot,server` (a comma-separated list of of profile names), then for each name supply the per-profile env-vars
profile names), then for each name supply the per-profile env-vars
prefixed with `CERTCTL_SCEP_PROFILE_<NAME>_` followed by the suffix prefixed with `CERTCTL_SCEP_PROFILE_<NAME>_` followed by the suffix
keys `_ISSUER_ID`, `_PROFILE_ID`, `_CHALLENGE_PASSWORD`, `_RA_CERT_PATH`, keys `_ISSUER_ID`, `_PROFILE_ID`, `_CHALLENGE_PASSWORD`, `_RA_CERT_PATH`,
`_RA_KEY_PATH`. The `<NAME>` token resolves to the upper-cased profile `_RA_KEY_PATH`. The `<NAME>` 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 `CERTCTL_SCEP_PROFILES` is unset). Per-profile preflight validates each
RA pair independently; failures log the offending PathID. RA pair independently; failures log the offending PathID.
### ChromeOS Admin Console pointer ## ChromeOS Admin Console pointer
In Google Admin Console → Devices → Networks → Certificates, register In Google Admin Console → Devices → Networks → Certificates, register
certctl's `/scep[/<pathID>]` URL as the SCEP server. Enter the challenge certctl's `/scep[/<pathID>]` 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 `GetCACert` first to retrieve the RA cert, then enrolls via
PKIOperation. PKIOperation.
### RA cert rotation ## RA cert rotation
The RA cert is loaded once at startup and persisted in the handler's The RA cert is loaded once at startup and persisted in the handler's
struct field; rotation requires a server restart (mirrors the 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 which both old + new RA certs are listed in `GetCACert`'s response (set
the cert chain accordingly in your sub-CA hierarchy). 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 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`, 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 SCEP profiles serving general / legacy clients (ChromeOS, IoT) should
stay `false` until the TLS path is verified. 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 SCEP is documented as application-layer-auth — the challenge password
is the authentication boundary per RFC 8894 §3.2. But enterprise 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 1. Generate a bootstrap CA + issue a bootstrap cert per device (out
of band — typically manufacturing-time, MDM-pushed, or a separate of band — typically manufacturing-time, MDM-pushed, or a separate
PKI flow). PKI flow).
2. Distribute the trust bundle to certctl as the 2. Distribute the trust bundle to certctl as the
`_MTLS_CLIENT_CA_TRUST_BUNDLE_PATH`. `_MTLS_CLIENT_CA_TRUST_BUNDLE_PATH`.
3. Set `_MTLS_ENABLED=true` for the profile, restart certctl. 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 password requirement doesn't go away — the password is still
the application-layer auth boundary). 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 When SCEP sits behind the Microsoft Intune Certificate Connector, devices
present an Intune-issued signed challenge (a JWT-like blob over a JSON 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 two dots) to the validator and falls through to the static compare
otherwise. otherwise.
### Operational notes ## Operational notes
- **Audit:** every enrollment emits an `audit_event` row with action - **Audit:** every enrollment emits an `audit_event` row with action
`scep_pkcsreq` (initial) or `scep_renewalreq` (renewal); operators `scep_pkcsreq` (initial) or `scep_renewalreq` (renewal); operators
@@ -498,8 +311,10 @@ otherwise.
bodies at `CERTCTL_MAX_BODY_SIZE` (default 1MB); SCEP PKIMessages are bodies at `CERTCTL_MAX_BODY_SIZE` (default 1MB); SCEP PKIMessages are
typically <50KB so the default cap is generous. typically <50KB so the default cap is generous.
- **HTTPS-only:** the SCEP endpoint inherits the TLS-1.3-pinned control - **HTTPS-only:** the SCEP endpoint inherits the TLS-1.3-pinned control
plane; there is no plaintext fallback. plane; there is no plaintext fallback. Legacy clients that only speak
- **For Microsoft Intune deployments, see [`scep-intune.md`](scep-intune.md)** 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 architecture, NDES-replacement migration playbook, Intune SCEP profile
field mapping, trust-anchor extraction recipe, troubleshooting matrix, field mapping, trust-anchor extraction recipe, troubleshooting matrix,
operational monitoring, V3-Pro deferrals, and the Microsoft support operational monitoring, V3-Pro deferrals, and the Microsoft support
@@ -508,12 +323,12 @@ otherwise.
mTLS sibling-route status, challenge-password-set indicator, and mTLS sibling-route status, challenge-password-set indicator, and
the full SCEP audit log filter), the admin GUI page lives at `/scep` the full SCEP audit log filter), the admin GUI page lives at `/scep`
with three tabs: **Profiles** (default), **Intune Monitoring**, with three tabs: **Profiles** (default), **Intune Monitoring**,
**Recent Activity**. See `scep-intune.md::Operational monitoring` **Recent Activity**. See the operational-monitoring section in
for the Intune-specific tab inside it. [`scep-intune.md`](scep-intune.md) for the Intune-specific tab.
## Related docs ## Related docs
- [`tls.md`](tls.md) — the certctl-internal TLS configuration (HTTPS-only - [`scep-intune.md`](scep-intune.md) — Microsoft Intune deployment guide
control plane, MinVersion pin) - [`est.md`](est.md) — EST RFC 7030 server reference
- [`security.md`](security.md) — overall security posture - [`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
- [`database-tls.md`](database-tls.md) — Postgres TLS opt-in (Bundle B / M-018) - [`docs/reference/architecture.md`](../architecture.md) — system design including SCEP server placement