From 347365ff9396c6940ceb83d422622a396727c6d0 Mon Sep 17 00:00:00 2001 From: certctl-copilot Date: Wed, 29 Apr 2026 03:53:00 +0000 Subject: [PATCH] ci+docs(scep): close G-3 docs-only drift for SCEP placeholder + wildcard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 1979047 (the prior docs fix for the multi-profile env vars) introduced two doc-only env-var literals that the G-3 scanner picked up as unmapped: * CERTCTL_SCEP_PROFILE_CORP_ISSUER_ID — the literal CORP example placeholder I added to clarify what the substitution looks like in practice. The G-3 scanner can't tell a placeholder from a real env var. * CERTCTL_SCEP_ — comes from the docs string CERTCTL_SCEP_* (the asterisk is not in [A-Z_], so the regex strips it down to the prefix and treats it as a phantom env var). Two-part fix: docs/features.md * Replaced the literal CORP example (CERTCTL_SCEP_PROFILE_CORP_ISSUER_ID) with a prose explanation that doesn't include a literal placeholder env var name. Operators still get a clear example via 'a CERTCTL_SCEP_PROFILES entry of corp resolves the issuer-id env var key with replaced by CORP'. .github/workflows/ci.yml * Added CERTCTL_SCEP_ to the G-3 ALLOWED prefix list, mirroring the existing CERTCTL_TLS_ entry. Both are legitimate doc-only prefix references (CERTCTL_TLS_* / CERTCTL_SCEP_*) that the scanner sees as bare prefixes after stripping the wildcard. The allowlist documents these as integration-surface contracts that the structured per-profile env vars expand into at runtime. Verification: local G-3 set difference (Go-defined ∖ docs-mentioned) empty in BOTH directions after the fix: * DOCS_ONLY (docs ∖ Go, post-allowlist): empty * CONFIG_ONLY (Go ∖ docs): empty Restores green CI on the env-var docs drift guard. --- .github/workflows/ci.yml | 1 + docs/features.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78f107a..3a96f61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1275,6 +1275,7 @@ jobs: CERTCTL_AUDIT_EXCLUDE_PATHS| CERTCTL_TLS_| CERTCTL_TLS_INSECURE_SKIP_VERIFY| + CERTCTL_SCEP_| CERTCTL_SERVER_CA_BUNDLE_PATH| CERTCTL_SERVER_TLS_INSECURE_SKIP_VERIFY| CERTCTL_QA_[A-Z_]+ diff --git a/docs/features.md b/docs/features.md index f72b6c0..e2a5832 100644 --- a/docs/features.md +++ b/docs/features.md @@ -649,7 +649,7 @@ SCEP uses a single URL (`/scep?operation=...`). The handler extracts PKCS#10 CSR | `CERTCTL_SCEP_RA_CERT_PATH` | (none) | Path to PEM-encoded RA (Registration Authority) certificate. **Required when `CERTCTL_SCEP_ENABLED=true`** for the RFC 8894 PKIMessage path: SCEP clients encrypt their PKCS#10 CSR to this cert's public key (EnvelopedData wrapper, RFC 8894 §3.2.2) and the server signs the outbound CertRep PKIMessage signerInfo with the matching key (RFC 8894 §3.3.2). Generation: a self-signed cert with `CN=-RA` and the `id-kp-emailProtection` / `id-kp-cmcRA` EKU is sufficient — see [`legacy-est-scep.md`](legacy-est-scep.md) for the openssl recipe. The preflight gate at startup also enforces a cert/key match, non-expired NotAfter, and an RSA-or-ECDSA public-key algorithm. | | `CERTCTL_SCEP_RA_KEY_PATH` | (none) | Path to PEM-encoded private key matching `CERTCTL_SCEP_RA_CERT_PATH`. **Required when `CERTCTL_SCEP_ENABLED=true`.** File MUST be mode `0600` (owner read/write only); preflight refuses to load a world- or group-readable RA key as defense-in-depth against credential leak. The server reads this file once at startup; rotation requires a restart. | | `CERTCTL_SCEP_PROFILES` | (none, single-profile mode) | Comma-separated list of SCEP profile names enabling **multi-endpoint dispatch** (Phase 1.5). When set, certctl exposes one `/scep/` endpoint per name (e.g. `CERTCTL_SCEP_PROFILES=corp,iot,server` produces `/scep/corp`, `/scep/iot`, `/scep/server`). Each name also drives the env-var prefix for the per-profile config below. When unset, certctl runs in legacy single-profile mode using the flat `CERTCTL_SCEP_*` env vars above (which synthesise a single-element profile bound to the legacy `/scep` root path). PathID must be a path-safe slug (`[a-z0-9-]`, no leading/trailing hyphen); names get lowercased for the URL path and uppercased for the env-var prefix. | -| `CERTCTL_SCEP_PROFILE__ISSUER_ID` | (none) | Per-profile issuer binding when `CERTCTL_SCEP_PROFILES` is set. `` is the upper-cased profile name from the list (e.g. `CERTCTL_SCEP_PROFILE_CORP_ISSUER_ID=iss-corp-laptop`). Same per-profile env-var prefix `CERTCTL_SCEP_PROFILE_` is also used for `_PROFILE_ID`, `_CHALLENGE_PASSWORD`, `_RA_CERT_PATH`, `_RA_KEY_PATH` — see the four rows below. Required for every profile listed in `CERTCTL_SCEP_PROFILES`. Each profile is independently validated at startup; per-profile failures log the offending PathID. | +| `CERTCTL_SCEP_PROFILE__ISSUER_ID` | (none) | Per-profile issuer binding when `CERTCTL_SCEP_PROFILES` is set. `` is the upper-cased profile name from the list (so a `CERTCTL_SCEP_PROFILES` entry of `corp` resolves the issuer-id env var key with `` replaced by `CORP`, the path-id `_ISSUER_ID` suffix unchanged). Same per-profile env-var prefix `CERTCTL_SCEP_PROFILE_` is also used for `_PROFILE_ID`, `_CHALLENGE_PASSWORD`, `_RA_CERT_PATH`, `_RA_KEY_PATH` — see the four rows below. Required for every profile listed in `CERTCTL_SCEP_PROFILES`. Each profile is independently validated at startup; per-profile failures log the offending PathID. | | `CERTCTL_SCEP_PROFILE__PROFILE_ID` | (none) | Per-profile optional `CertificateProfile` constraint, mirroring the legacy `CERTCTL_SCEP_PROFILE_ID`. Leave unset to allow the issuer's defaults. | | `CERTCTL_SCEP_PROFILE__CHALLENGE_PASSWORD` | (none) | Per-profile shared secret. **Required for every profile** in `CERTCTL_SCEP_PROFILES` (CWE-306: per-profile auth boundary). Empty value at startup fails the boot with the offending PathID in the structured log. | | `CERTCTL_SCEP_PROFILE__RA_CERT_PATH` | (none) | Per-profile RA certificate PEM path. Same semantics as `CERTCTL_SCEP_RA_CERT_PATH` but scoped to one profile. **Required for every profile.** |