Files
shankar0123 56e2ea1ad7 docs: v2.1.0 release polish — strip internal bundle/phase tags, update status for OIDC ship
README:
- Rewrite Status block: drop the stale 'federated identity not yet
  shipped' line; flag v2.1.0 OIDC + sessions + back-channel logout
  + break-glass as early-access; encourage GitHub issues for IdP
  rough edges. (A1 framing — keep early-access umbrella, no
  SAML/WebAuthn/JIT roadmap teaser.)
- Add OIDC SSO bullet to 'What it does' covering per-IdP runbooks,
  group-claim → role mapping, AES-256-GCM client_secret encryption,
  JWKS auto-refresh, PKCE-S256, RFC 9700 §4.7.1 pre-login binding,
  RFC 9207 iss check, __Host- cookies, CSRF rotation, idle+absolute
  expiry, BCL, break-glass admin.
- Update Security paragraph: three auth paths (API keys / OIDC /
  break-glass), HMAC-signed sessions, CSRF rotation, RFC OIDC BCL.
- Correct CI coverage thresholds against
  .github/coverage-thresholds.yml (service 70%, handler 75%,
  crypto 88%, auth packages 85-95%); 'static analysis' replaces
  the inflated '11 linters' claim (actual count is 4 active).

Docs B3 sweep — strip operator-facing 'Bundle N' / 'Phase N' tags:
- docs/operator/auth-threat-model.md — rewrite intro; rename 5 H2
  sections (API-key + RBAC defenses / OIDC + sessions + break-glass
  defenses / OIDC + sessions threat catalogue / Closed federated-
  identity threats / Future-work threats); clean ~12 H3/prose hits.
- docs/operator/rbac.md — strip Bundle 1 framing from intro,
  scope_id deferral note, MCP tools section, day-0 bootstrap, and
  'Where to look next'.
- docs/operator/auth-benchmarks.md — drop 'Phase 14' framing from
  title intro, hardware floor caption, result table caption,
  methodology, and pre-merge audit section.
- docs/operator/security.md — already cleaned earlier this session
  (RBAC / day-0 / approval-bypass / OIDC federation / sessions /
  OIDC first-admin / break-glass H3s).
- docs/operator/oidc-runbooks/{index,keycloak,authentik,okta,
  azure-ad}.md — strip Auth Bundle 2 framing + Phase 10/3/4
  references; replace with feature-name prose.
- docs/operator/legacy-clients-tls-1.2.md — drop Bundle F / M-023
  audit-reference framing; keep CWE-326.
- docs/operator/database-tls.md — drop Bundle B / M-018 framing
  from intro + Helm section.
- docs/operator/runbooks/disaster-recovery.md — drop 'Production
  hardening II Phase 10' status callout.
- docs/migration/oidc-enable.md — retitle 'Enable OIDC SSO';
  strip Bundle 1/2 framing from prereqs, troubleshooting, related
  docs; update __Host- cookie callout from 'audit MED-14' to
  v2.1.0-BREAKING.
- docs/migration/api-keys-to-rbac.md — strip Bundle 1 framing from
  intro, migration table, IsAdmin section, and cross-references.
- docs/migration/acme-from-cert-manager.md — strip residual
  'Phase 5' tags from cert-manager integration test references.
- docs/reference/configuration.md — retitle Auth section.
- docs/reference/profiles.md — strip Bundle 1 Phase 9 framing
  from RequiresApproval section + Related list.
- docs/reference/auth-standards-implemented.md — rewrite intro
  (API-key + RBAC + OIDC + sessions + back-channel logout +
  break-glass); rename 'Bundle 1 (RBAC) standards covered
  separately' H2; clean per-row Phase references.
- docs/README.md — rewrite nav-table entries to drop Bundle 1/2
  parentheticals; retitle 'Enable OIDC SSO' migration entry.

No code or test changes; pure operator-facing prose polish for
the v2.1.0 tag.
2026-05-11 16:54:07 +00:00

113 lines
5.6 KiB
Markdown

# 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 at v2.1.0.
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. |
| `requires_approval` | false | 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 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 profile-edit 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.
## Related
- `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` (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)