mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:01:34 +00:00
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:
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user