mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:41:30 +00:00
8908c8ff5c
Final commit of the 5-commit Rank 8 chain. Operator-facing surface
on top of the service + handler layers shipped in commits 1-4.
Frontend (web/src):
- api/client.ts: 3 new functions + IntermediateCA interface
(listIntermediateCAs, getIntermediateCA, retireIntermediateCA).
- pages/IssuerHierarchyPage.tsx: recursive nested <ul> render of
the hierarchy tree at /issuers/:id/hierarchy. buildHierarchyTree
is a pure helper that walks the flat list and groups children
on parent_ca_id; the dendrogram view is parking-lot work tracked
in WORKSPACE-ROADMAP. Two-phase retire UX surfaces 'Retire…'
then 'Confirm retire (terminal)' when the row is in retiring
state. Admin gate is enforced at the API; the page renders the
backend's 403 as ErrorState for non-admin callers.
- main.tsx: register the new /issuers/:id/hierarchy route.
CI guard update:
- scripts/ci-guards/T-1-frontend-page-coverage.sh: add
IssuerHierarchyPage to the deferred-test allowlist with the
standard 'why deferred' comment. Admin-gate + recursive build
semantics are already pinned at the backend layer
(intermediate_ca_test.go service tests + intermediate_ca_test.go
handler triplet). Vitest test deferred until next feature
change touches the page.
Docs:
- docs/intermediate-ca-hierarchy.md: new operator runbook
covering:
Concepts (HierarchyMode 'single' vs 'tree', defense-in-depth
on key bytes never persisting on rows).
Lifecycle states + drain-first semantics
(active → retiring → retired with active-children gate).
Three deployment patterns: 4-level FedRAMP boundary CA,
3-level financial-services policy CA, 2-level internal
PKI.
RFC 5280 enforcement (§3.2 self-signed, §4.2.1.9 path-length
tightening, §4.2.1.10 NameConstraints subset).
Migration from single → tree using the load-bearing
TestLocal_HierarchyMode_SingleVsTree_ByteIdentical pin as
the canary.
API reference + observability (IntermediateCAMetrics
Prometheus exposure).
Known limitations + Rank-8 follow-on roadmap.
- docs/connectors.md: extend the Built-in Local CA section with
a 'Tree mode (Rank 8)' paragraph describing the new chain
assembly path + cross-link to docs/intermediate-ca-hierarchy.md.
Roadmap:
- WORKSPACE-ROADMAP.md: 5 follow-on items under a new
'Intermediate CA hierarchy extensions (Rank 8 V2 follow-ons)'
bullet block:
HSM-backed roots (PKCS#11 / cloud KMS drivers via existing
signer.Driver interface — no service-layer change needed).
Automated CA rotation (parallel-validity windows ahead of
expiry).
Intra-hierarchy CRL chaining (per-CA CRL endpoints stitched
at issue time).
NameConstraints policy templates (FedRAMP / financial /
internal PKI declarative templates instead of hand-rolled
JSON).
D3 dendrogram visualization (separate page so the existing
list view stays the default + the dep stays opt-in).
Verified locally:
gofmt: clean.
go vet ./...: exit 0.
tsc --noEmit (web/): exit 0 (no TypeScript errors).
go test -short -count=1 ./internal/api/handler/... + service +
local: ok across all three packages, 4-5s each.
All 24 CI guards: clean
(T-1 frontend-page-coverage with the new
IssuerHierarchyPage allowlist entry; openapi-handler-parity,
M-008 admin-gate, every other guard untouched).
Rank 8 chain complete:
66d2af3 domain, migrations: IntermediateCA type + intermediate_cas
+ Issuer.HierarchyMode (commit 1)
fb54ebc service: IntermediateCAService + IntermediateCAMetrics
+ RFC 5280 enforcement (commit 2)
62523fb service: 10 IntermediateCAService tests + in-memory fake
repo (commit 2.5)
ae597f7 local: tree-mode chain assembly + byte-equivalence pin
(commit 3 — load-bearing backwards-compat refuse-to-ship
pin in TestLocal_HierarchyMode_SingleVsTree_ByteIdentical)
34adcfb api, handler: 4 admin-gated CA hierarchy endpoints +
OpenAPI (commit 4)
HEAD web, docs: IssuerHierarchyPage + sysadmin runbook +
connectors row (this commit)
Reference: cowork/rank-8-intermediate-ca-hierarchy-prompt.md, commit 5.
scripts/ci-guards/ — Regression-guard scripts
Each <id>.sh script in this directory pins one closed audit finding from
regressing. CI runs the full set on every push via the
Regression guards step in .github/workflows/ci.yml. Operators can
run any script locally:
bash scripts/ci-guards/G-3-env-docs-drift.sh
Contract
Every script in this directory MUST:
- Be exit-code 0 on a clean repo (no regression present).
- Be exit-code non-zero on regression, with a
::error::annotation prefix so PR reviewers see the failing line in the GitHub Actions UI. - Be runnable from repo root via
bash scripts/ci-guards/<id>.shwith NO arguments and NO env-var requirements. The CI loop step (for g in scripts/ci-guards/*.sh; do bash "$g"; done) iterates every.shhere without args; any script that requires an arg or env var WILL fail in that loop. - Carry a head-comment block matching the in-source justification from the original ci.yml entry: the audit-finding reference, the closure rationale, the exempt-surface list (if any).
- Use
set -eearly to fail-fast on internal command errors. - Produce no output on the happy path beyond a final
echo "<id>: clean."confirmation line.
Helpers vs guards
Scripts that consume input artifacts (a test-output log, a
coverage.out file) or env vars (PR_NUMBER, GH_TOKEN) are
HELPERS, not guards. They live in scripts/, NOT scripts/ci-guards/.
Current helpers:
scripts/vendor-e2e-skip-check.sh— consumestest-output.logarg from the deploy-vendor-e2e jobscripts/coverage-pr-comment.sh— consumescoverage.out+PR_NUMBER+GH_TOKENenv from the go-build-and-test jobscripts/check-coverage-thresholds.sh— consumescoverage.out.github/coverage-thresholds.yml
scripts/qa-doc-part-count.sh+scripts/qa-doc-seed-count.sh— invoked viamake verify-docspre-tag, not in CI
Adding a new guard
- Drop a new
<id>.shin this directory with the head-comment block describing the audit finding it closes. - Make it executable:
chmod +x scripts/ci-guards/<id>.sh. - Verify it fails on a deliberate regression and passes on clean repo.
- CI auto-picks up new scripts via the
for g in scripts/ci-guards/*.shloop in theRegression guardsstep — no ci.yml change required.
The 22 guards in this directory
| ID | Finding | Catches |
|---|---|---|
G-1-jwt-auth-literal |
G-1 JWT silent auth downgrade | "jwt" literal in additive auth-type surfaces |
L-001-insecure-skip-verify |
L-001 unjustified InsecureSkipVerify | InsecureSkipVerify: true without //nolint:gosec |
H-001-bare-from |
H-001 (CWE-829) tag-swap attack | Bare FROM line without @sha256 digest pin |
M-012-no-root-user |
M-012 (CWE-250) container-as-root | Dockerfile missing terminal USER <non-root> |
H-009-readme-jwt |
H-009 README JWT advertising | README.md re-introducing JWT-as-supported claim |
G-2-api-key-hash-json |
G-2 cat-s5-apikey_leak | api_key_hash in JSON-emitting surface |
U-2-plaintext-healthcheck |
U-2 healthcheck protocol mismatch | Plaintext http:// in HEALTHCHECK directive |
U-3-migration-mount |
U-3 seed initdb schema drift | Migration file mounted into postgres initdb |
D-1-D-2-statusbadge-phantom |
D-1 + D-2 dead keys + TS phantoms | StatusBadge dead keys + 5 Certificate / 5 Agent / 1 Issuer / 1 Notification phantom fields |
L-1-bulk-action-loop |
L-1 client-side bulk loops | for ... await triggerRenewal/updateCertificate in CertificatesPage |
B-1-orphan-crud |
B-1 orphan-CRUD client fns | 8 update/create/delete fns lose their page consumer |
S-2-strings-contains-err |
S-2 brittle error-dispatch | strings.Contains(err.Error(), "not found"|"violates foreign key") in handlers |
G-3-env-docs-drift |
G-3 env-var docs drift | CERTCTL_* env var defined OR documented but not both |
test-naming-convention |
I-001-extended | func TestXxx (lowercase first letter) — Go silently skips |
S-1-hardcoded-source-counts |
S-1 stale numeric prose | Hardcoded "N issuer connectors" / "N MCP tools" in README + docs |
P-1-documented-orphan-fns |
P-1 documented orphans | 16 read-fn names removed from client.ts exports |
T-1-frontend-page-coverage |
T-1 untested frontend pages | New page in web/src/pages/ without sibling .test.tsx and not on the deferred allowlist |
bundle-8-L-015-target-blank-rel-noopener |
L-015 (CWE-1022) reverse-tabnabbing | target="_blank" without rel="noopener noreferrer" |
bundle-8-L-019-dangerously-set-inner-html |
L-019 (CWE-79) XSS | dangerouslySetInnerHTML outside safeHtml.ts |
bundle-8-M-009-bare-usemutation |
M-009 + M-029 mutation contract | Bare useMutation() outside useTrackedMutation wrapper |
H-1-encryption-key-min-length |
H-1 closure follow-up (post-Phase-5 surfacing) | CERTCTL_CONFIG_ENCRYPTION_KEY literal in any deploy/docker-compose*.yml shorter than the 32-byte floor enforced by internal/config/config.go::Validate() |
test-compose-scep-coherence |
post-Phase-5 surfacing of dead SCEP test config | CERTCTL_SCEP_ENABLED=true in test compose without (a) a CI job that runs the SCEP integration test, (b) the ra.crt + ra.key + intune_trust_anchor.pem fixtures committed to deploy/test/fixtures/, AND (c) the matching volume mount |
Guards explicitly NOT here
QA-doc Part-count drift+QA-doc seed-count drift— these protect docs-the-operator-reads, not anything the product depends on. Moved tomake verify-docs(operator runs pre-tag, not on every push). Seecowork/ci-pipeline-cleanup-prompt.mdPhase 11.
Running the full set locally
for g in scripts/ci-guards/*.sh; do
echo "=== $(basename "$g") ==="
bash "$g" || echo " FAILED"
done