Closes the last Phase before the Bundle 1 Exit gate. Operators
now have authoritative reference + threat model + migration guide
covering every behavior change Bundles 0-12 introduced.
# New docs
* docs/operator/rbac.md (340 lines) — operator how-to:
- Mental model (actors / roles / permissions / scopes)
- 7 default roles seeded by migration 000029 + the 5
admin-only fine-grained perms seeded by 000030
- Permission catalogue table by namespace
- Scope semantics (global beats specific) + the Bundle-2
deferral on scope_id FK enforcement
- Granting / revoking access from GUI + CLI + HTTP API + MCP
- The auditor pattern (audit-only, no resource read)
- Day-0 bootstrap flow (CERTCTL_BOOTSTRAP_TOKEN → curl →
HTTP 410 thereafter)
- Demo-mode (CERTCTL_AUTH_TYPE=none) caveat for production
* docs/operator/auth-threat-model.md (180 lines) — what the
controls defend against:
- 5 threat actors (external, wrong-role, compromised key,
insider operator, compromised auditor)
- Per-defense walk-through (API-key auth, RBAC, bootstrap,
approval workflow + Phase 9 closure, audit trail,
protocol-endpoint allowlist)
- 9 explicit deferrals (OIDC, sessions, local accounts,
JIT elevation, MFA, etc.) — Bundle 2 / future scope
- Compliance mapping (SOC 2 CC6.1/CC6.3, HIPAA §164.312(b),
NIST SSDF PO.5.2, FedRAMP AU-9, PCI-DSS §10)
- 5 operator-runnable sanity checks (e.g.,
'SELECT FROM audit_events WHERE actor=system-bypass' MUST
return 0 in production)
* docs/migration/api-keys-to-rbac.md (200 lines) — v2.0.x →
v2.1.0 upgrade flow:
- The SECURITY: AUDIT YOUR API KEYS callout
- Migration list (000029-000033) + what each does
- 4-mode scope-down flow (interactive / non-interactive
JSON / --suggest / --suggest --apply)
- What changes for code that called auth.IsAdmin
- Helm-specific upgrade flow with example post-upgrade Job
- Docker Compose upgrade flow + the 5 examples folders
that ride demo mode unchanged
- Verification queries + rollback flow
# Updated docs
* docs/operator/security.md — Last-reviewed bumped to
2026-05-09; existing Authentication-surface section
extended to call out the Bundle 1 RBAC primitive,
day-0 bootstrap path, and approval-bypass closure with
cross-references to the new docs.
* docs/reference/profiles.md — Last-reviewed header
formatting fixed (added the > blockquote prefix used
consistently across the docs tree).
# docs/README.md navigation
* Operator section gains 2 new rows (RBAC + auth-threat-model)
and Approval-workflow row updated to mention Phase 9
closure.
* Reference section gains the Profiles row.
* Migration section gains the api-keys-to-rbac row with the
AUDIT YOUR API KEYS callout in the link description.
# CHANGELOG.md v2.1.0 section refreshed
The Phase 7 commit landed the SECURITY: AUDIT YOUR API KEYS
callout. This commit appends the missing Phase 9-12 highlights:
- Approval-bypass closure (profile-edit gate + flip-flop
loophole + ErrApproveBySameActor invariant)
- GUI: Roles / API Keys / Auth Settings / Approvals queue
- 12 new MCP RBAC tools
- Coverage gates on internal/auth + internal/service/auth
- Protocol-endpoint allowlist pinned at 3 layers
Trailing cross-reference block now points at all 4 new docs.
# Verifications
* Every internal link in the 4 new/modified docs validated by
shell sweep (find broken links → 0 hits).
* Every new doc carries 'Last reviewed: 2026-05-09' header
with the > blockquote prefix matching the docs-tree
convention.
* go vet ./... clean.
* staticcheck across every Bundle-1-touched Go package clean.
* gofmt -l clean repo-wide.
* go test -short -count=1 green across internal/auth (incl.
bootstrap), internal/api/handler, internal/api/router,
internal/cli, internal/service (incl. auth),
internal/domain/auth, internal/mcp, cmd/cli (cmd/server
has 1 environmental failure on the sandbox virtiofs-tmp:
TestPreflightSCEPRACertKey_KeyWorldReadable_Refuses depends
on tmpfs file-mode semantics that virtiofs propagates
differently — pre-existing, unrelated to Bundle 1).
* Frontend: 19 Vitest tests across src/pages/auth/ +
AuditPage all pass; tsc --noEmit clean.
7.4 KiB
Changelog
v2.1.0 — Auth Bundle 1: RBAC primitive ⚠️
SECURITY: AUDIT YOUR API KEYS.
Bundle 1 ships role-based authorization. Every existing API key configured via
CERTCTL_API_KEYS_NAMED(or the legacyCERTCTL_AUTH_SECRET) is mapped to the r-admin role on the first upgrade boot so existing automation keeps working unchanged. Most keys do NOT need full admin power; downgrade them before tagging the next release.Recommended post-upgrade flow:
# 1. List every key with its current role: certctl-cli auth keys list # 2. Walk an interactive prompt that downgrades each key: certctl-cli auth keys scope-down # 3. Or get a heuristic suggestion based on 30 days of audit history: certctl-cli auth keys scope-down --suggest certctl-cli auth keys scope-down --suggest --apply # applies the suggestion # 4. Or drive scope-down from a JSON config (Helm post-upgrade hook): certctl-cli auth keys scope-down --non-interactive ./scope-down.jsonThe synthetic
actor-demo-anonactor (used whenCERTCTL_AUTH_TYPE=noneis configured) is system-managed and excluded from the prompt loop.
What else changed in v2.1.0:
- RBAC primitive shipped.
tenants,roles,permissions,role_permissions,actor_rolestables (migration 000029); 33-permission canonical catalogue; 7 default roles (admin,operator,viewer,agent,mcp,cli,auditor); per-handler permission gates viaauth.RequirePermissionmiddleware (replaces the legacyIsAdminboolean check on the 5 admin-only handlers). - Day-0 admin bootstrap. Set
CERTCTL_BOOTSTRAP_TOKENon a fresh deploy and POST a single curl call against/api/v1/auth/bootstrapto mint the first admin API key; one-shot, never logged, and locks closed once any admin actor exists. Migration 000031 ships theapi_keystable that stores the SHA-256 hash; the plaintext is shown in the response body once and never persisted. - Auditor role split. New
auditorrole holds onlyaudit.readaudit.export. Compliance reviewers can read the audit trail without holding mutation power. Migration 000032 addsaudit_events.event_categoryso auditors can filter to authentication-related events specifically.
/v1/auth/checkenrichment. Response now includes the actor's standing roles and effective permissions, so the GUI gates affordances from a single fetch on app boot.- Approval-bypass closure. Edits to a profile that has (or
would have)
RequiresApproval=truenow route through theApprovalServicetwo-person integrity gate (Phase 9). Migration 000033 addsapproval_kind+payloadtoissuance_approval_requestsso cert-issuance and profile-edit approvals share the same workflow. Same-actor self-approve is rejected withErrApproveBySameActorfor both kinds. Closes the flip-flop loophole where an admin could disable approval, mutate, re-enable. Documented atdocs/reference/profiles.md. - GUI: Roles / API Keys / Auth Settings / Approvals queue.
Four new pages under
/auth/*consume/v1/auth/mefor permission-aware rendering. The Approvals queue blocks self-approve at the client layer (Approve/Reject buttons hidden when requested_by == current actor_id) on top of the server-side enforcement. AuditPage gains a category filter (cert_lifecycle / auth / config) for the auditor view. - MCP server gains 12 RBAC tools. Operators driving certctl
from Claude / VS Code / any MCP client get parity with the GUI
- CLI. Each tool routes through the same HTTP handler; permission gates fire server-side.
- OpenAPI catalogues every new route. Every Bundle 1 endpoint
ships with an
operationId; the parity test guards against drift. - Coverage gates.
internal/auth/andinternal/service/auth/now have ≥85% coverage floors in.github/coverage-thresholds.yml. The 12-path negative-test list from the Bundle 1 prompt is fully covered (path #12 deferred with in-tree TODO). - Protocol-endpoint allowlist pinned at three layers. The
middleware bypass (
auth.IsProtocolEndpoint), the router-levelAuthExemptRouterRoutesconstant, and a newphase12_protocol_allowlist_test.goAST scan all guard against accidentally wrapping ACME / SCEP / EST / OCSP / CRL routes inrbacGate. - Bundle 2 (OIDC + sessions) starts after Bundle 1 lands on
master. Roadmap entry remains in
cowork/auth-bundle-2-prompt.md.
Migration ordering, idempotency, and downgrade are documented in
docs/migration/api-keys-to-rbac.md.
The threat model + compliance mapping live at
docs/operator/auth-threat-model.md.
Day-2 RBAC operations live at
docs/operator/rbac.md.
v2.0.68 — Image registry path changed ⚠️
Image registry path changed. Starting this release, container images publish to
ghcr.io/certctl-io/certctl-serverandghcr.io/certctl-io/certctl-agent. Existing pulls fromghcr.io/shankar0123/certctl-{server,agent}:<tag>continue to work for previously-published tags (the registry never deletes images), but the:latesttag at the old path stops moving forward at this release. Update yourdocker pullpaths,docker-compose.ymlimage:keys, or Helmimage.repositoryvalues to receive future updates. Oldgit clone/git push/ install-script / API URLs continue to redirect forever — only the container-registry path changed.
This is the only operator-action-required change in v2.0.68. Other changes in this release are cosmetic URL refreshes after the GitHub-org transfer from shankar0123/certctl to certctl-io/certctl (HTTP redirects mean no other operator action is required) plus an internal contextcheck lint fix in the agent. Full commit list is on the GitHub release page.
certctl no longer maintains a hand-edited per-version changelog. Per-release notes are auto-generated from commit messages between consecutive tags.
Where to find what changed in a given release:
- GitHub Releases — every tag has an auto-generated "What's Changed" section pulled from the commits between that tag and the previous one, plus per-release supply-chain verification instructions (Cosign / SLSA / SBOM).
git log <prev-tag>..<this-tag> --oneline— same content, locally.
Why no hand-edited CHANGELOG.md:
certctl is solo-developed and pushes directly to master. Maintaining a
hand-edited CHANGELOG meant the file drifted (entries piled into
[unreleased] and never got promoted to per-version sections when tags were
cut). A stale CHANGELOG is worse than no CHANGELOG — it signals abandoned
maintenance to security-conscious operators doing diligence.
The auto-generated release notes work here because commit messages follow a
descriptive convention: <area>: <summary> with a longer body for non-trivial
changes (see git log v2.0.50..HEAD for the established pattern). Anyone
reading the GitHub Releases page can see exactly what landed in each version
without depending on the author to manually update a separate file.
For the historical record: earlier versions (pre-v2.2.0 and the [2.2.0] tag itself) had a hand-edited CHANGELOG. That content is preserved in git history at the v2.2.0 tag.