Self-audit caught five real gaps in 3ef45e2; this commit closes them. # Phase 8 — issuer/target audit rows now classified as 'config' The Phase 8 prompt explicitly required existing config-mutation calls (issuer config, target config, etc.) to write event_category=config. The3ef45e2commit only migrated the auth service callers; the 6 issuer/target call-sites (internal/service/issuer.go: create/update/delete_issuer + internal/service/target.go: create/update/delete_target) still defaulted to cert_lifecycle. They now pass through RecordEventWithCategory(..., domain.EventCategoryConfig, ...) so auditors filtering /v1/audit?category=config see the slice the migration's docstring promised. # Auditor exit-criterion test Phase 8's exit criteria pin 'a user with the auditor role can list / export audit events but gets 403 on every other endpoint.' Bundle 1 unit invariants (auditor permission set, rbacGate behaviour) were in place but no end-to-end test walked the full set of admin perms with an auditor actor. internal/api/router/rbac_gate_integration_test.go gains TestRBACGate_AuditorRole_403sOnAdminRoutes (table-driven across all 5 admin perms — cert.bulk_revoke / crl.admin / scep.admin / est.admin / ca.hierarchy.manage) plus TestRBACGate_AuditorRole_PassesAuditReadGate (positive case for audit.read). # gofmt drift3ef45e2left two cosmetic struct-field-alignment diffs in internal/cli/auth.go and internal/api/handler/audit_handler_test.go that gofmt -l flagged. CI's gofmt step would have failed; gofmt -w applied; gofmt -l now clean across the repo. # CHANGELOG path-prefix CHANGELOG.md v2.1.0 used '/v1/auth/bootstrap' shorthand in the operator-facing flow examples. The actual route is '/api/v1/auth/bootstrap'; an operator copy-pasting the curl would 404. All five hits replaced. Verifications: gofmt clean, go vet ./internal/service/ ./internal/api/router/ clean, go test -short -count=1 green across internal/service + internal/api/router, including the 6 new auditor sub-tests (PASS).
5.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.- OpenAPI catalogues every new route. Every Bundle 1 endpoint
ships with an
operationId; the parity test guards against drift. - 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.
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.