Skip to content

feat(operators): credential requests, recording and denylist revoke - #152

Merged
Bugs5382 merged 6 commits into
mainfrom
feat/147-operator-credential-requests
Oct 1, 2026
Merged

Bugs5382 merged 6 commits into
mainfrom
feat/147-operator-credential-requests

Conversation

@Bugs5382

@Bugs5382 Bugs5382 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The manager never signs an operator credential, so the old issue path is replaced by request, sign at the external CA, record, and deny. This serves the RPCs the contract already had as stubs.

Closes #147

  • Requests: CreateOperatorCredentialRequest (admin) checks the CSR with CheckCSR (subject exactly CN=<email>, P-384 or RSA 3072+, the request's email), stores it pending for 30 days, and returns the CSR PEM, the op_<level> OpenSSL section built from authz.MarshalLevelExtension, and the openssl ca command. ListOperatorCredentialRequests (operator, optional state filter) and CancelOperatorCredentialRequest (admin) are served. A pending request past its expiry reads as expired; the CSR is dropped on completion, cancellation or expiry.
  • Record: RecordOperatorCredential (admin) runs CheckOperatorCert. With request_id the level, CN and key must match the pending request, which is completed in the same transaction as the insert (kind requested); without it an out-of-band certificate is imported (kind recorded). It must be signed by the active operator CA (NOT_ACTIVE_ANCHOR for the retiring one) and not already recorded (DUPLICATE). A row only observed in use is upgraded in place.
  • Revoke: RevokeOperatorCredential already wrote the denylist with issuer and note. It now refuses a serial that isn't hex, a reason that isn't an RFC 5280 CRL reason (0-10 except 7) and an issuer that isn't a SHA-256 fingerprint, instead of writing them. The audit summary says the credential is not revoked at the CA.
  • List: ListOperatorCredentials also returns first_seen_at and last_seen_at.
  • Observed credentials: with Postgres, the API path's peer authorizer records each certificate it accepts as an observed row. An LRU keeps a certificate from being written more than hourly, and a bounded queue drops a sighting when full without blocking the request; a dropped or failed sighting is retried on the next request.
  • Everything is audited (operator-credential-requested, operator-credential-request-cancelled, operator-credential-recorded, operator-revoked). The new RPCs stay excluded from MCP, as the catalog already marked them.
  • The in-memory store refuses requests and recording with 1603 DATABASE_REQUIRED.

Docs: docs/operator-credentials.md is new; docs/operator-pki.md (the node-hosted operator CA) is removed and its level extension table moved to the new page; links in the README, operator-ca.md and deploying-standalone.md are updated. Docs site companion: CryptOS-PKI/docs#73.

No new dependencies.

Merge order: independent of #150, #151 and #149 (trial merges with each are clean). Merge together with the docs companion.

Verification

  • Lint clean
  • Tests pass
  • Build succeeds
  • Documentation updated (if behavior or API changed)

How this was verified

task ci (gofmt, vet, golangci-lint, go test ./... with a fresh test cache) passes with MANAGER_TEST_DATABASE_URL pointing at a throwaway Postgres 18, so the store integration tests ran. task license passes. 36 new tests, each seen failing first: the Postgres store suite, the handler tests (admin gating, CSR and name refusals, request matching, 1611 states, active-anchor and duplicate refusals, the next request refused after a denial), the recorder tests (one write per certificate, hourly refresh, non-blocking overflow, retry after a failed write) and the wiring tests.

An OpenSSL 3 run of the documented flow (a P-384 CSR on the holder side, openssl ca with the returned op_operator section, openssl pkcs12 -export, openssl ca -revoke and -gencrl) produced a certificate with the level extension non-critical, and CheckCSR plus CheckOperatorCert with the CSR accepted it at level operator.

Credential requests, recording against a request and observed credentials
get their own store interface. Postgres keeps them in the v7 tables:
recording and completing a request happen in one transaction, a pending
request past its expiry reads as expired with its CSR dropped, and a row
only observed in use is upgraded when the certificate is recorded. The
in-memory store refuses them with ErrDatabaseRequired.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
An observed-credential recorder wraps the peer authorizer and queues one
observed row per certificate it accepts. An LRU keeps a certificate from
being written more than hourly, and a bounded queue drops a sighting when
full so a request never waits; a dropped or failed sighting is retried on
the certificate's next request.

Also adds the op_<level> OpenSSL section and signing command built from
the level encoding the manager reads, and the full-name check.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
CreateOperatorCredentialRequest checks the CSR, stores it pending for 30
days and returns the CSR, the level's extension section and the openssl
command. RecordOperatorCredential checks a signed certificate against the
request (level, CN and key) or imports one made out of band; it must come
from the active operator CA and not be recorded already. Listing and
cancelling requests are served too.

RevokeOperatorCredential now refuses a serial that isn't hex, a reason
that isn't an RFC 5280 CRL reason and an issuer that isn't a SHA-256
fingerprint, and ListOperatorCredentials returns first and last seen.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
With Postgres configured, the API's peer authorizer records every
operator certificate it accepts through the observed-credential recorder.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
A new page covers requesting a credential, signing it at the operator CA,
recording it, credentials seen in use, and denying one, with the warning
that a Fleet Manager denial doesn't revoke at the CA. operator-pki.md,
which described the node-hosted operator CA, is removed; its level
extension table moves to the new page.

Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
@Bugs5382 Bugs5382 added this to the v0.1.0 milestone Sep 30, 2026
@Bugs5382 Bugs5382 added the enhancement New feature (feat). Minor version bump. label Sep 30, 2026
@Bugs5382 Bugs5382 self-assigned this Sep 30, 2026
@Bugs5382
Bugs5382 marked this pull request as ready for review September 30, 2026 23:35
Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
@Bugs5382

Bugs5382 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Merging for the v0.1.0 test run: CI green and mergeable at merge time.

@Bugs5382
Bugs5382 merged commit cd3a52f into main Oct 1, 2026
9 checks passed
@Bugs5382
Bugs5382 deleted the feat/147-operator-credential-requests branch October 1, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature (feat). Minor version bump.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(operators): credential requests, recording and denylist revoke

1 participant