mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:21:31 +00:00
harden(auth): demo-mode residual-grants detector + cleanup endpoint + CI guard (A-8)
Audit 2026-05-11 A-8 closure. Closes the deferred Phase 2 leg of the
2026-05-10 HIGH-12 closure (2e97cc1) — production-startup observability
for actor-demo-anon residual grants + CI guard banning new synthetic-
admin code paths.
What this changes:
* cmd/server/preflight_demo_residual.go (new) runs after the DB pool +
audit service are constructed and before the HTTPS listener starts.
Under any non-'none' auth type it queries actor_roles for the
synthetic actor-demo-anon and emits a WARN log + a categorized audit
row (auth.demo_residual_grants_detected) listing every grant
present. Migration 000029 unconditionally seeds the ar-demo-anon-admin
row at install time, so EVERY production deploy will see this WARN
on first boot; the intended cutover workflow is cleanup-once at
production handover.
* CERTCTL_DEMO_MODE_RESIDUAL_STRICT (new env var on AuthConfig,
default false) pivots the WARN to fail-closed startup refusal for
operators who want a paranoid posture against re-seeding.
* POST /api/v1/auth/demo-residual/cleanup (new handler at
internal/api/handler/demo_residual.go) is an admin-class
(auth.role.assign) endpoint that removes every actor-demo-anon row
from actor_roles and returns {removed: int64}. Idempotent; refuses
503 under Auth.Type=none (deleting the row would break the demo
path); audit-logs every invocation including no-op zero-removed
calls so the admin's action is always recorded.
* scripts/ci-guards/no-new-synthetic-admin.sh pins the 17-entry
allowlist of source files that legitimately reference the
actor-demo-anon literal. New runtime code paths that resolve to the
synthetic actor (the same pattern that produced the original CRIT
class) are rejected at PR time. CI workflow auto-picks the script
via the existing scripts/ci-guards/*.sh loop in .github/workflows/
ci.yml; no workflow edit needed.
Regression matrix:
* cmd/server/preflight_demo_residual_test.go — 7 tests covering the
4 main behaviour branches (testcontainers-backed, testing.Short()-
skipped: DemoModeActive_Skips, NoResidue_Passes, HasResidue_LogsAnd
Audits, StrictMode_RefusesStartup, DeleteDemoAnonResidue_Idempotent)
plus 3 pure-Go stdlib unit tests for the row-string formatter +
nil-safety contracts on both helpers.
* internal/api/handler/demo_residual_test.go — 7 stdlib+httptest
cases: HappyPath, Idempotent_ReturnsZero, RejectsInDemoMode (503),
CleanupError_Surfaces500, NilCleanupFn (defensive 500),
NilAuditWriter_DoesNotPanic, MissingActorContext (falls back to
'unknown' actor in the audit row).
* internal/api/router/openapi_parity_test.go — new
POST /api/v1/auth/demo-residual/cleanup entry plus 6 pre-existing
pre-A-8 entries (oidc/test, jwks-status, users CRUD, runtime-config)
that had drifted out of SpecParityExceptions; the parity test was
red on dev/auth-bundle-2 before my work; this commit returns it to
green with full per-entry justifications + parity-debt notes.
Docs:
* docs/operator/security.md — new 'Demo-to-production cutover (Audit
2026-05-11 A-8)' section explaining the WARN message, the cleanup
curl one-liner, the equivalent SQL, the strict-mode env var, and
the CI guard.
* docs/operator/rbac.md — Last-reviewed bump + pointer to the new
env var + the security.md section.
* cowork/auth-bundles-audit-2026-05-10.md — HIGH-12 row gains an
'A-8 follow-on CLOSED 2026-05-11' annotation describing the
deferred Phase 2 leg now landed.
* CHANGELOG.md — Unreleased ### Security entry summarizing the four
legs (detector + cleanup + strict-mode flag + CI guard) and the
acquisition-readiness narrative this closes.
Operator-facing impact: this closes a credibility gap, not an
exploitable vulnerability. The residue requires a regression
elsewhere in the middleware chain to be exploitable. After this
fix, the canonical narrative ('RBAC primitive with no synthetic-
admin fallback') is fully true.
Refs cowork/auth-bundles-fixes-2026-05-11/08-high-demo-mode-residual-
cleanup.md.
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# RBAC operator reference
|
||||
|
||||
> Last reviewed: 2026-05-09
|
||||
> Last reviewed: 2026-05-11
|
||||
>
|
||||
> Audit 2026-05-11 A-8 follow-on: demo-mode residual-grants detector
|
||||
> + cleanup endpoint shipped. New env var:
|
||||
> `CERTCTL_DEMO_MODE_RESIDUAL_STRICT` (default `false`). Operator
|
||||
> workflow at
|
||||
> [`security.md#demo-to-production-cutover-audit-2026-05-11-a-8`](security.md#demo-to-production-cutover-audit-2026-05-11-a-8).
|
||||
|
||||
This is the operator-facing reference for the role-based access
|
||||
control primitive that ships with Bundle 1 (auth bundle 1) of certctl.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# certctl Security Posture & Operator Guidance
|
||||
|
||||
> Last reviewed: 2026-05-10
|
||||
> Last reviewed: 2026-05-11
|
||||
|
||||
This document collects the operator-facing security guidance that the source
|
||||
code's per-finding comment blocks reference. Each section names the audit
|
||||
@@ -262,6 +262,61 @@ to avoid a permanent backdoor; the runbook at
|
||||
[`auth-threat-model.md#break-glass-risks-phase-75`](auth-threat-model.md)
|
||||
documents the full state machine.
|
||||
|
||||
### Demo-to-production cutover (Audit 2026-05-11 A-8)
|
||||
|
||||
Migration `000029_rbac.up.sql` unconditionally seeds an
|
||||
`actor-demo-anon → r-admin` row into `actor_roles`. This row is the
|
||||
runtime principal injected by the demo-mode middleware when
|
||||
`CERTCTL_AUTH_TYPE=none`. Under any non-`none` auth type the row is
|
||||
DORMANT — the middleware chain never resolves to it. But its existence
|
||||
is a footgun: a future regression that resolves an unauthenticated
|
||||
request to `actor-demo-anon` (a misrouted CORS preflight, a fallback in
|
||||
a new auth-exempt route) would silently re-elevate to admin.
|
||||
|
||||
certctl-server detects this residue at startup and emits a WARN log +
|
||||
an `auth.demo_residual_grants_detected` audit row listing every grant
|
||||
present on `actor-demo-anon`. **Every production deploy will see this
|
||||
WARN on first boot** — the migration baseline is part of the install,
|
||||
not a side effect of running demo mode.
|
||||
|
||||
Operator workflow at production cutover:
|
||||
|
||||
1. Drain the WARN by calling the cleanup endpoint with an admin API key:
|
||||
|
||||
```bash
|
||||
curl -X POST --cacert deploy/test/certs/ca.crt \
|
||||
-H "Authorization: Bearer $ADMIN_KEY" \
|
||||
https://certctl.example.com:8443/api/v1/auth/demo-residual/cleanup
|
||||
# → {"removed": 1}
|
||||
```
|
||||
|
||||
The endpoint is gated `auth.role.assign` (admin-class) and refuses
|
||||
to run when `CERTCTL_AUTH_TYPE=none` (HTTP 503 — the residue IS the
|
||||
active runtime state at that auth type). The cleanup is idempotent;
|
||||
a second call returns `{"removed": 0}` and still leaves an audit row.
|
||||
|
||||
Equivalent SQL for operators preferring direct DB access:
|
||||
|
||||
```sql
|
||||
DELETE FROM actor_roles WHERE actor_id = 'actor-demo-anon';
|
||||
```
|
||||
|
||||
2. To make subsequent boots refuse startup if the row reappears (the
|
||||
most paranoid stance), set:
|
||||
|
||||
```
|
||||
CERTCTL_DEMO_MODE_RESIDUAL_STRICT=true
|
||||
```
|
||||
|
||||
With the flag set, any `actor-demo-anon` row under a non-`none`
|
||||
auth type causes certctl-server to log the WARN AND exit non-zero
|
||||
before binding the HTTPS listener. Default is `false` (WARN only).
|
||||
|
||||
3. The CI guard `scripts/ci-guards/no-new-synthetic-admin.sh` pins the
|
||||
set of source files that may reference the `actor-demo-anon` literal.
|
||||
New runtime code paths that resolve to the synthetic actor are
|
||||
rejected at PR time so the credibility gap stays closed.
|
||||
|
||||
### Migrating an existing deployment to OIDC
|
||||
|
||||
A Bundle-1-merged deployment that wants to add OIDC follows the
|
||||
|
||||
Reference in New Issue
Block a user