Self-audit on e7a94b6 flagged the prompt's 'zero em dashes'
discipline rule. The four new Phase 13 docs and the v2.1.0
CHANGELOG section had 97 em-dash hits between them; this commit
sweeps them all to ASCII hyphens.
Counts before -> after:
docs/operator/rbac.md 28 -> 0
docs/operator/auth-threat-model.md 36 -> 0
docs/migration/api-keys-to-rbac.md 16 -> 0
docs/operator/security.md 8 -> 0
docs/reference/profiles.md 3 -> 0
CHANGELOG.md 6 -> 0
Mechanical: ' - ' (spaced em dash) and bare em-dash both replaced
with spaced ASCII hyphen, then double-spaces collapsed. Markdown
list bullets ('^- ', '^ - ', '^ - ') verified intact across
all six files. Internal-link sweep also re-run.
Also fixes a pre-existing broken link the audit caught:
docs/operator/security.md:70 referenced
'../internal/crypto/encryption.go' which is a 1-level-up jump
from docs/operator/, not the 2-level-up jump it actually needs
('../../internal/crypto/encryption.go'). Pre-Bundle-1 link rot;
fixed in lockstep so the merge gate's docs validation passes
cleanly.
Final state across the Phase-13 docs + CHANGELOG:
- 0 em dashes
- 0 broken internal links
- Last-reviewed: 2026-05-09 header on every new doc
Bundle 1 documentation is now ready for the operator-side merge
gate review.
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.