mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:51:29 +00:00
a923cf697c
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.
75 lines
3.8 KiB
Bash
Executable File
75 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Audit 2026-05-11 A-8 — no new code paths may reference actor-demo-anon
|
|
# outside the declared allowlist. The synthetic actor is a load-bearing
|
|
# demo-mode primitive but ANY new reference in production code paths is
|
|
# a candidate footgun (the original CRIT class was a fallback that
|
|
# resolved unauthenticated requests to this actor and got full admin).
|
|
#
|
|
# Adding a legitimate new reference? Add the file to ALLOWLIST below
|
|
# AND describe the reason in this header. Operators (auditors) read
|
|
# this script to understand where the synthetic admin "lives" in the
|
|
# codebase.
|
|
#
|
|
# Test files (*_test.go), /vendor/, /docs/, and CHANGELOG entries are
|
|
# excluded — they don't introduce new runtime code paths.
|
|
|
|
set -euo pipefail
|
|
|
|
# Files that legitimately reference the actor-demo-anon literal in
|
|
# source. Each entry needs a one-line rationale comment so future
|
|
# maintainers don't have to trace why it's here.
|
|
ALLOWLIST=(
|
|
"./cmd/server/main.go" # HandlerRegistry comment + DemoResidual wiring
|
|
"./cmd/server/preflight_demo_residual.go" # A-8 detector + cleanup helpers
|
|
"./internal/api/handler/auth.go" # interface docstring for ListKeys
|
|
"./internal/api/handler/demo_residual.go" # A-8 cleanup endpoint
|
|
"./internal/api/router/router.go" # routing comment for cleanup endpoint
|
|
"./internal/auth/context.go" # const DemoAnonActorID source-of-truth (canonical)
|
|
"./internal/auth/middleware.go" # NewDemoModeAuth — injects synthetic actor under Type=none
|
|
"./internal/cli/auth_scope_down.go" # interactive prompt filter
|
|
"./internal/config/config.go" # validate-time guard comments + DemoModeResidualStrict env var
|
|
"./internal/domain/audit.go" # audit-event documentation comment
|
|
"./internal/domain/auth/validate.go" # const DemoAnonActorID mirror
|
|
"./internal/mcp/tools_auth.go" # MCP tool description for ListKeys + Revoke
|
|
"./internal/mcp/types.go" # MCP request-schema description
|
|
"./internal/repository/auth.go" # ActorRoleRepository interface docstrings
|
|
"./internal/service/auth/actor_role_service.go" # reserved-actor mutation guard (CRIT-1 closure)
|
|
"./internal/service/auth/authorizer.go" # synthetic-actor authorization comment
|
|
"./scripts/ci-guards/no-new-synthetic-admin.sh" # this script itself
|
|
)
|
|
|
|
declare -A allow=()
|
|
for loc in "${ALLOWLIST[@]}"; do allow["$loc"]=1; done
|
|
|
|
violations=()
|
|
# rg/grep with -l prints filenames. We exclude test files, vendored
|
|
# code, docs (operator-facing prose), and CHANGELOG markdown.
|
|
while IFS= read -r file; do
|
|
[ -z "$file" ] && continue
|
|
if [ -z "${allow[$file]:-}" ]; then
|
|
violations+=("$file")
|
|
fi
|
|
done < <(grep -rln 'actor-demo-anon' \
|
|
--include='*.go' --include='*.sh' . \
|
|
2>/dev/null \
|
|
| grep -v '_test\.go$' \
|
|
| grep -v '^\./vendor/' \
|
|
| grep -v '^\./docs/' \
|
|
| grep -v '^\./CHANGELOG\.md$' \
|
|
| sort -u)
|
|
|
|
if [ ${#violations[@]} -gt 0 ]; then
|
|
printf 'A-8 GUARD FAIL: new actor-demo-anon reference outside the established allowlist:\n'
|
|
printf ' %s\n' "${violations[@]}"
|
|
printf '\n'
|
|
printf 'If this reference is legitimate, add the file to ALLOWLIST in\n'
|
|
printf ' scripts/ci-guards/no-new-synthetic-admin.sh\n'
|
|
printf 'WITH a rationale comment describing why the synthetic admin\n'
|
|
printf 'literal needs to appear there. Otherwise, route through the\n'
|
|
printf 'public DemoAnonActorID constant or refactor the new code path\n'
|
|
printf 'to NOT reference the synthetic actor at all (preferred).\n'
|
|
exit 1
|
|
fi
|
|
|
|
echo "A-8 guard PASS — actor-demo-anon references confined to the declared ${#ALLOWLIST[@]}-entry allowlist."
|