Files
certctl/docs/reference/profiles.md
T
shankar0123 ba68f9a994 auth-bundle-1 Phase 13: docs (rbac.md + threat model + migration guide + security.md update)
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.
2026-05-10 00:10:15 +00:00

5.8 KiB

Certificate profiles

Last reviewed: 2026-05-09

A CertificateProfile is the policy object that groups every cert with the same shape: which issuer mints it, which key algorithm + size are allowed, what EKUs and SANs the issuer should emit, what renewal window the scheduler uses, what targets get the cert deployed to. Every managed certificate references exactly one profile; changing a profile's policy retroactively affects renewal of every cert pointing at it.

This file documents the profile lifecycle as it stands after Bundle 1. For the schema, see migrations/000003_certificate_profiles.up.sql + migrations/000027_approval_workflow.up.sql + migrations/000033_approval_kinds.up.sql. For the API surface, see api/openapi.yaml under /api/v1/profiles.

Anatomy

Field Default Purpose
id autogenerated prof-<slug> Stable opaque identifier; used by every other resource.
name required Human-readable label; rendered in the GUI's profile picker.
issuer_id required Which issuer (Local / Vault / EJBCA / ACME / SCEP / EST / ADCS / etc.) mints certs against this profile.
default_validity_days 90 Rendered into the issuer call as the requested NotAfter delta.
renewal_window_days 30 Scheduler enqueues a renewal Job when cert.NotAfter - now < renewal_window_days.
allowed_key_algorithms RSA 2048+, ECDSA P-256+ Validates incoming CSRs at issuance time.
allowed_ekus server, client RFC 5280 §4.2.1.12 EKU set.
must_staple false Per-profile RFC 7633 id-pe-tlsfeature extension toggle (Phase 5.6 of the SCEP master bundle).
requires_approval false Bundle 1 Phase 9 — gates issuance + renewal AND profile edits behind a four-eyes approval workflow. See below.

RequiresApproval and the approval workflow

Setting requires_approval=true on a profile does two things:

  1. Issuance + renewal of every cert pointing at the profile gates on a non-requester admin's approval. The scheduler enqueues a Job at status AwaitingApproval; the linked issuance_approval_requests row stays at pending until either approved (job → Pending, scheduler dispatches) or rejected (job → Cancelled). Same actor cannot self-approve.
  2. Edits to the profile itself gate on a non-requester admin's approval. This is the Bundle 1 Phase 9 closure for the flip-flop loophole — without it an admin could set requires_approval=false, mutate any other field, set requires_approval=true, and the approval workflow would only have been bypassed during the "off" window. The Phase 9 gate fires under three conditions:
    • The live profile has requires_approval=true AND the operator submits any edit (regardless of whether the edit changes the flag).
    • The live profile has requires_approval=false AND the operator submits an edit that would set it to true (the flag-flip direction is gated too because otherwise the gate could be enabled by anyone and have no review).
    • Both arms route through ApprovalService.RequestProfileEditApproval which writes a row to issuance_approval_requests with approval_kind=profile_edit. The pending profile diff is serialized to payload (JSONB).

Edit response shape. When the gate fires, PUT /api/v1/profiles/{id} returns HTTP 202 Accepted with body {"status":"pending_approval","pending_approval_id":"ar-…"}. The operator copies the approval ID, hands it to a peer admin, and the peer POSTs /api/v1/approvals/{id}/approve with their own credentials. On approve, the server deserializes payload, applies the diff against the live profile, and emits a profile.edit_applied audit row with event_category=auth. On reject, the pending row is dropped; the live profile is unchanged.

Same-actor self-approve is rejected with HTTP 403 and the existing ErrApproveBySameActor sentinel. This is the load-bearing two-person-integrity invariant that satisfies SOC 2 CC6.3 + NIST SSDF PO.5.2.

Bypass mode. CERTCTL_APPROVAL_BYPASS=true short-circuits both issuance approvals and profile-edit approvals; every request auto-approves with actor=system-bypass. Used by dev / CI for fast iteration; production deploys MUST leave it unset. A single SQL query (SELECT FROM audit_events WHERE actor='system-bypass') confirms zero rows.

Operator workflows

Enable approval for an existing profile. Edit the profile, set requires_approval=true. The first time you do this, the edit itself is gated (the live profile is non-approval but the proposed state is approval-tier, so the flip-on direction still routes through the workflow). Hand the approval ID to a peer; once approved, every subsequent edit and every renewal of every cert pointing at the profile gates on the workflow.

Disable approval. Edit the profile, set requires_approval=false. This edit is gated because the live profile is currently approval-tier. A peer must approve the disable. Once disabled, subsequent edits flow through the direct-apply path again.

Audit who approved what. The audit trail records every approval request + decision under event_category=auth. Filter via GET /api/v1/audit?category=auth or the auditor role's audit-only view. Each row carries the approval ID + the requester

  • the decider; the WORM trigger prevents tampering.
  • migrations/000027_approval_workflow.up.sql (initial approval schema, Rank 7 of the 2026-05-03 deep-research deliverable)
  • migrations/000033_approval_kinds.up.sql (Phase 9 — adds approval_kind + payload + nullable cert/job FKs)
  • internal/service/approval.go::RequestProfileEditApproval
  • internal/service/profile.go::UpdateProfile (gate)
  • internal/api/handler/profiles.go::UpdateProfile (202 mapping)
  • cowork/auth-bundle-1-prompt.md (Phase 9 spec)