Policy & governance — decide what's allowed, prove what happened
What it is
Governance decides whether an action may happen, who may do it, which runtime attributes narrow that permission, who gets told, and what record is kept. trstctl's governance is six capabilities: a policy engine that allows or denies each operation, RBAC that enforces who can do what, an ABAC deny overlay that blocks requests using environment, time, actor, and resource attributes, notifications that alert the right people, a tamper-evident audit log that records everything, and compliance reporting that turns that record into signed evidence for auditors.
The mental model: the rulebook, the ID checkpoint, the pager, the flight recorder, and the auditor's evidence pack — controls that turn a powerful tool into one a regulated enterprise can run.
Why it exists
A credential platform is, by definition, powerful — it can mint and revoke the identities holding your infrastructure together. That power needs guardrails: a way to encode "never issue a 10-year cert," ensure only authorized people issue, know immediately when something important happens, and keep an unforgeable record for the inevitable audit. Without these, the platform is a liability; with them, it's what proves your machine-identity hygiene to a customer's security team.
How it works
The policy engine (F28)
When ca.policy.enabled is true, every issue, deploy, and revoke transition
passes through an embedded OPA / Rego policy gate before it
executes. The configured boot module compiles at startup; an invalid module is a
hard startup error. Each tenant can author and activate a replacement through the
Policy console. A draft has no effect on decisions. Each decision sees structured input (action, profile, actor,
tenant_id, attributes) and is fail-closed: an evaluation error, ambiguous result, or
overloaded pool all return deny. Evaluation runs in its own bounded lane, so overload
is rejected fast instead of starving issuance, and every decision is recorded as an
immutable policy.decision event. The default policy denies everything except
revocation and permits issuance/deployment only when a profile is bound.
Status: served on the issuance path when ca.policy.enabled: the gate runs on every
issue/deploy/revoke transition, the RA scope split (certs:request ≠ certs:issue)
stops a requester from self-issuing, and ca.policy.require_approval requires a distinct
approver — self-approval is rejected.
Approvals count people, not token names. An API token that one person mints through the
API for a different subject can do that subject's work, but every approval and denial
route (identity, issuance-request, access-change, secret, managed-key, profile and
ephemeral approvals, and CA key-ceremony approvals, including break-glass quorums)
refuses it with 403. Approve with your own sign-in or with a token you minted for
yourself. Tokens a deployment administrator creates with trstctl token create sit
inside that administrator's custody boundary and are not treated as delegated.
The active rule is read from that tenant's immutable policy events before a mutation decision. Compiled rules are cached, but a cache is never permission: a restart or another server instance reads the same active version. Activating or rolling back a rule in one tenant cannot change another tenant's rule. An unreadable event history or a module/hash mismatch denies the operation instead of silently reverting to the boot rule.
POST /api/v1/policy/versions creates a draft. POST /api/v1/policy/versions/{id}/activate makes it active; POST /api/v1/policy/versions/{id}/rollback restores its immediately preceding rule as
a new recorded version. These writes require policy:write, an
Idempotency-Key, and a reason for activation or rollback. GET /api/v1/policy/versions requires policy:read and lists only the current tenant's
history. Concurrent changes are serialized before recording their predecessor.
Disabling ca.policy.enabled disables this gate; a readable policy history alone
does not prove enforcement is enabled. The list response includes
enforcement.enabled from this running server and, when enabled,
enforcement.module_sha256 for the tenant's verified runtime rule. If that rule
cannot be read or disagrees with the returned active version, the read fails
instead of reporting protection. Enabled means the rule is evaluated; it does
not certify that its allow/deny logic is safe.
Certificate posture rules. The same module may declare a
certificate_posture set. The rogue-certificate listing
(GET /api/v1/revocation/rogue-certificates) evaluates it once per inventory
certificate with input.action == "certificate_posture" and input.certificate
carrying public metadata (subject, issuer, serial, fingerprint,
dns_names, key_algorithm, not_before, not_after, lifetime_days,
days_until_expiry, status, source, owner_id, ca_id,
deployment_location). Every object the set yields is one violation:
certificate_posture contains {"rule": "vendor-ca-forbidden", "severity": "high",
"reason": "leaves from the vendor CA are not sanctioned"} if {
input.action == "certificate_posture"
input.certificate.issuer == "CN=Untrusted Vendor CA"
}
A posture rule may also ask for a remediation: "remediation": "renew" in the
yielded object tells the lifecycle scheduler to queue a renewal for a deployed
X.509 identity whose served certificate fires the rule, so the non-compliant
certificate is replaced the way an expiring one is. The scheduler runs it under
the same automation switch (lifecycle.renew_before) and maintenance-window
deferral as expiry renewals, bounds it to 100 identities per tenant per sweep,
skips identities with renewal work in flight or an unverified delivery, and
records the rule in the transition reason (posture:<rule>). Only renew is
recognized; any other value queues nothing. The listing shows each violation's
remediation.
NHI posture rules (CAP-POST-06). The same module may declare an nhi_posture
set over one NHI inventory row's merged posture: input.action == "nhi_posture" and
input.nhi carrying inventory_id, kind, source, display_name, owner_id,
owner_status, status, managed (a managed identity row), posture (the families
that fired: overprivilege, stale, static_credentials, exposure),
finding_types, severity, risk_score, activity_age_days, credential_age_days,
rotation_age_days, and the granted_scopes, unused_scopes and
recommended_scopes of usage-backed over-privilege. A rule may ask for one of two
remediations:
nhi_posture contains {"rule": "dormant-managed-revoke", "severity": "high",
"reason": "dormant managed NHIs are revoked", "remediation": "revoke"} if {
input.action == "nhi_posture"
input.nhi.managed
"dormant_activity" in input.nhi.finding_types
}
"revoke" revokes the managed identity behind the row, and only an issued, deployed,
renewing or renewal_failed one, through the same lifecycle gate as the identity's own
revoke: the sweep acts as system:nhi-posture, holding certs:issue, so the ABAC
overlay and this module's own revoke decision see that actor and can deny it, and
dual control refuses it because an automatic action carries no exact approval. The
revocation reason is RFC 5280 cessationOfOperation. "right_size" queues the
right-size playbook's connector.right_size intent for the row's usage-backed unused
grants, once per row, rule and exact scope set. The leader's lifecycle sweep acts on
them under the automation switch and maintenance-window deferral the renewals use, at
most 25 actions per tenant per sweep. Each action is a remediation playbook run
created by system:nhi-posture whose reason is posture:<rule> and whose evidence
names the rule and the module digest. A gate refusal is recorded the same way (status
refused) and retried after 24 hours. Any other remediation value is reported and
never acted on. GET /api/v1/nhi/posture/automation (trstctl-cli nhi posture automation, and the Risk page panel) shows each match's action (revoke,
right_size, report_only, or not_applicable with the reason) and its latest
automatic run, and POST /api/v1/policy/dry-run with
{"action": "nhi_posture", "tenant_id": ..., "nhi": {...}} previews a candidate rule.
The lifecycle allow/reason are not consulted for posture, so a module that
declares no posture rules reports nothing. Posture evaluation is a read and is
not written to the policy.decision audit stream; the listing names the module
digest it evaluated. POST /api/v1/policy/dry-run with
{"action": "certificate_posture", "tenant_id": ..., "certificate": {...}}
returns the violations a candidate module would fire.
The console keeps drafts separate from active authority, starts change and evidence references blank, and reviews the exact rule before saving. A failed save remains visible inside the editor without discarding the draft. Activation and rollback carry the selected version's recorded evidence references; an unsubmitted editor cannot supply audit evidence for another rule. Runtime capabilities disable these writes when the live engine is not configured.
The version response also reports rollback_available: true only for an active
version with a recorded predecessor. A newly restored version has no earlier
rollback target. Its Rollback control is disabled; an operator can explicitly
activate a chosen historical version instead. If that restored record is later
activated again, it gains a predecessor and can be rolled back. The console
uses the served field, not the description or rollback_from_id label. Older
idempotent response bodies may omit the new field; absence means unknown, so the
console waits for the current version list before offering rollback.
RBAC (F8)
Role-based access control decides who may do what. Permissions are
<resource>:<verb> strings (certs:issue, audit:read); five built-in roles ship —
admin, operator, viewer, auditor, and ra-officer (which can request but not
self-issue, the registration-authority separation). Grants are scoped to a
tenant, and a scope check hard-blocks cross-tenant access at the database layer, so one
tenant can never read another's. The API's guard middleware checks the required
permission on every route, returns 403 application/problem+json on failure, and stamps
the acting principal into the immutable event record for audit attribution.
Status: enforced on every served route.
Ownership attribution
Every non-human identity needs an accountable owner, not a discovery note that
says "owner: unknown." GET /api/v1/ownership/attribution (nhi:read) resolves managed
and discovered NHIs to a registered owner, team, or vendor, and marks anything
unresolved as orphaned rather than counted as accountability. See
Discovery & inventory for the resolution mechanics, owner
kinds, and evidence-ref shape; the same data is available via
trstctl-cli owners attribution and the Owners console.
Ownership attribution is also deployment authority. Owner create/update carries
application_id, service, business_unit, environment, and an ordered
escalation chain in owner.* v2 events. POST /api/v1/owners/{id}/attest records
the authenticated principal and a digest of the exact application/environment
model. A deployment event carries either that still-current proof or one active,
reasoned, attributed exception from
/api/v1/identities/{id}/ownership-exceptions; it is rejected with 409 when
neither exists. The configurable cadence scheduler requests re-attestation once
per stale verification edge and writes the owner/escalation notification intent
through the outbox in the same tenant transaction.
For an urgent identity with incomplete ownership, open Ownership → Coverage gaps
and temporary exceptions, select the exact identity, and review any existing
exception before granting another. The panel reads the attributed reason,
grantor, expiry and state from the server. An active exception can be revoked
there with a required reason; the console then reads the server's revoked
record. To inspect a past exception after the identity leaves the gap queue,
enter its inventory or incident Identity ID in the same panel and choose
Inspect exception history. That lookup is read-only for grants but still
allows a reasoned revocation if the server reports an active exception. API and
CLI equivalents are GET and POST
/api/v1/identities/{id}/ownership-exceptions and POST
/api/v1/identities/{id}/ownership-exceptions/{exception_id}/revoke, or
trstctl owners exceptions list|grant|revoke. The CLI requires --force for
revocation. Expiry or revocation removes this temporary admission proof; it
does not repair the owner record or provide a permanent waiver.
Status: served for managed identities plus discovery-fed NHIs.
For a one-time ownership CSV import, create the owner record first, then run
trstctl owners import -f ownership.csv. The file needs an owner column; it may
also carry application_id, service, business_unit, environment, and
source_ref. The CLI sends it as text/csv. An import records disagreements in
trstctl owners ownership-conflicts; resolve each one with a reason using
trstctl owners resolve-conflict <id> -f request.json. A CSV row cannot invent
an owner or overwrite an attested field. The decision target must be an
existing, open disagreement in the caller's tenant: an unknown ID returns 404,
and a second decision with a new idempotency key returns 409 rather than
claiming to change or overwrite the first decision. Retrying the original
idempotency key returns its original receipt.
The served OpenAPI declares this import as a required text/csv body (up to
8 MiB). Scheduled ServiceNow reconciliation is configured with
PUT /api/v1/owners/cmdb-schedule using JSON: an operator-approved HTTPS instance
URL, a secret:// token reference, an interval of at least 300 seconds, and
relay execution. GET /api/v1/owners/cmdb-schedule distinguishes never
configured, paused, incomplete, failed, and completed coverage.
POST /api/v1/owners/cmdb-schedule/run (trstctl owners cmdb-schedule run)
starts a sweep now, as the scheduler's own durable relay job, without waiting
for the interval; it refuses a paused schedule and never stacks a read behind a
sweep that is still in flight.
A CI's owner reference is matched to a trstctl owner by its email first
(owned_by.email, assigned_to.email, support_group.email are read beside
the display values) and by its display name second, so an estate whose owner
names differ from the CMDB's labels still reconciles when the mailboxes agree.
A CI matching neither is counted as unattributed, never created.
The deployment binding for this URL and reference must set relay_only: true;
ticket and response-dispatch bindings remain separate env: references.
For a private HTTPS CMDB whose issuing CA is absent from the network relay's
system trust store, configure that agent with --relay-ca-bundle /path/to/cmdb-ca.pem as described in Configuration.
The control-plane --ca-bundle authenticates trstctl only; it does not grant
trust to CMDB servers. A failed TLS verification remains a failed schedule
attempt until the correct CA is in the bundle file (the relay re-reads a changed
file without a restart and reports the bundle in force on the Agents page) and
the next due job completes.
The Ownership console exposes Configure CMDB sync in its Sources disclosure
even before the first schedule exists. Review the exact URL, reference, query,
interval, and enabled state before saving. An ambiguous response can be retried
with the same reviewed idempotency key. The console requires owners:write to
offer setup and shows the server's binding refusal if the URL and reference were
not approved in deployment configuration.
In the console, open Ownership → Sources, disagreements, and review history
to select a CSV, review its name and size, and apply it. The result separates
applied values, unchanged values, and disagreements; resolve the latter from
the queue below it. The browser retains the same idempotency key with the
selected file after an uncertain response, so retrying does not make a second
decision.
NHI policy compliance
GET /api/v1/nhi/policy/compliance (nhi:read) evaluates governed non-human
identities — managed and discovery-fed — against policy facts in the unified NHI
inventory: rotation cadence, allowed scopes/geographies, expiry/maximum TTL, and
business purpose.
The response is a posture view, not the raw source record: counts for compliant and
violating NHIs, per-control violation totals, severity, risk score, disallowed
scopes/geographies, recommendation text, and evidence refs such as inventory:<id> and
discovery.finding:<id>. Raw credential values are never returned. A row isn't counted
as compliant because it exists — it becomes governed only once it carries a rotation/TTL
policy, a scope or geography envelope, an expiry, or business-purpose metadata.
The same read path is available as trstctl-cli nhi policy compliance; the Risk console
shows the highest-severity violations beside over-privilege, stale, and
static-credential posture.
Status: served for metadata-backed policy compliance over managed and discovered NHIs; authoring and activating the live Rego module is the deployment-time workflow under the policy engine above.
Automated NHI decommissioning
POST /api/v1/nhi/decommission (identities:write) accepts governance signals
from owner departure, vendor termination, or inactivity review and resolves them
against tenant-local managed NHI identities. The matcher understands direct
identity_id, owner_id, owner name/email, vendor name, and metadata-only
activity fields such as last_seen_at and last_used_at. Matched identities are
decommissioned through the normal lifecycle state machine: active issued,
deployed, or renewing identities are revoked with the RFC 5280
cessationOfOperation reason by default, and already-revoked identities are
retired. Each revocation takes the same lifecycle gate as the identity's own
revoke: the caller needs certs:issue, and the ABAC overlay and default-deny
policy must allow it. A decommission request carries no exact approval, so where
dual control applies the identity is reported refused with the gate's reason
and left as it was; revoke it through POST /api/v1/identities/{id}/transitions
with a distinct approver's approval. The response is an evidence pack with the
signal type, action, previous state, final state, and evidence refs for each
item, and its summary counts refused apart from failed.
The same workflow is exposed as trstctl-cli nhi decommission -f nhi-decommission.json --force and in the Identities console. It deliberately
does not treat full tenant deletion or member/API-token offboarding as NHI
decommissioning; those remain separate admin flows.
In Machine identities → Decommission by signal, Departure offers two explicit selectors. Owner name or email matches the recorded owner and can affect all of that owner's managed identities. Exact identity ID selects one tenant-local identity; use it when the change ticket names a specific machine identity. A zero-match response leaves credentials unchanged and asks the operator to check the selector; it is not presented as a successful decommission. Review the returned per-identity actions, then verify certificate revocation using the signed CRL or OCSP with a relying-party client where applicable.
Status: served for managed NHI identities selected by departure, vendor-term, and inactivity signals, using event-sourced revoke/retire transitions.
Access-change approvals
Access-change approvals answer "who approved this non-human identity change, and which
change record justified it?" POST /api/v1/access/requests (access:write) opens a
tenant-scoped request: action (grant, modify, revoke, rotate, deploy, or
break_glass), NHI identifier and kind, resource, entitlement, PR/ticket/CAB reference,
reason, risk, evidence refs, and required approval count. trstctl infers the change
system from GitHub, GitLab, Bitbucket, ServiceNow, or Jira references; unknown systems
stay external, not a supported integration.
Decisions are recorded with POST /api/v1/access/requests/{id}/decisions
(access:write): the requester and approver are derived from their respective
authenticated sessions. Optional body requester_subject and approver_subject
claims must match those sessions. The approver must differ from the requester;
a second decision by the same approver is rejected before an event is appended.
The request reaches approved only at quorum (any denial moves it to denied).
If an older build already appended a duplicate decision, cold replay retains
the first decision by that approver, logs the ignored event ID and sequence,
and continues serving. The duplicate remains in immutable event history;
live decisions still reject it.
An approval may leave its decision reason blank; a denial requires one.
Create and decide are idempotent, so a retried
Idempotency-Key returns the original result rather than duplicating an approval. The
read side is projected from access.change_request.created and
access.change_request.decided events, available at GET /api/v1/access/requests and
GET /api/v1/access/requests/{id} (access:read).
trstctl-cli access requests create -f access-request.json
trstctl-cli access requests list --status pending
trstctl-cli access requests get <request-id>
trstctl-cli access requests decide <request-id> -f access-decision.json
The Policy console shows recent requests, their PR/ticket/CAB evidence, approval count, and approve/deny actions, never displaying credential material. An approver enters their own decision evidence references in the decision panel. The console does not copy the request's evidence into an approval; the readback shows what that approver actually submitted. A refused decision appears beside that request; it does not change the independent lifecycle-policy protection summary. If the request list cannot be read, the approval count says unavailable rather than reporting zero.
Status: served for PR/ticket/CAB-backed access-change requests with distinct-approver enforcement, idempotency, event projection, API, CLI, and console coverage.
ABAC deny overlay
Attribute-based access control (ABAC) narrows a permission RBAC already granted. It's one-way: ABAC can deny, but never grants a route or certificate action the caller didn't hold through RBAC. RBAC answers "is this caller allowed in principle?"; ABAC answers "is this exact request allowed right now?"
Enable it with auth.abac.enabled and a Rego module that declares
package trstctl.abac. The module evaluates after RBAC on every guarded API route with
request attributes (input.permission, input.resource.request.method,
input.resource.request.path, optional project, actor roles, configured input.env,
and time fields such as input.now_hour_utc); on the served issue/deploy/revoke
lifecycle route it also adds identity attributes such as identity.id, identity.kind,
identity.status, owner_id, transition.to, input.resource.env, and
input.resource.tags.service.
ABAC fails closed: a non-compiling module stops startup, an evaluation error denies with
403, a saturated policy worker lane returns 503, and every decision is recorded as an
immutable policy.abac.decision event. A practical change-window overlay — deny
certs:issue in prod outside a configured window — is shown in full under
Use it.
Status: served after RBAC on every guarded route when auth.abac.enabled, plus
identity-attribute evaluation on issue/deploy/revoke.
The audit log (F9)
Buyer receipt for CAP-POL-06 Tamper-evident / signed audit log: the served audit
surface is GET /api/v1/audit/events for tenant-scoped replay and GET /api/v1/audit/export for a signed offline evidence bundle.
The console names this surface Change history and starts with a bounded event summary rather than raw event machinery. It does not call that window the newest or complete history. Search activity opens filters, rows, and event detail. The plain search is case-insensitive across event type, privacy-filtered actor subject and roles, and event data. Hash-chain status and signed or ingestible exports live under Signatures and evidence export; scheduled SIEM feeds live under Collector delivery. All three sections preserve the same served endpoints and tenant scope, and explicit query links open the search layer automatically.
The audit log is a hash-chained, tamper-evident record where each entry's hash links
to the previous one (hash_i = SHA256(hash_{i-1} || record_i); all hashing goes through
the single crypto path). Altering, dropping, or reordering any record breaks the chain,
and VerifyChain names the first broken link — offline. Every change is recorded as an
immutable event, and the audit log is a rebuilt view of that history, not a separate
write store, so it can't drift from what actually happened; it's tenant-isolated at the
database layer. You can export a JOSE-signed evidence bundle an auditor verifies without
touching the live system, and retention checkpoints keep the chain verifiable even after
old segments are archived.
Status: served — GET /api/v1/audit/events and GET /api/v1/audit/export.
Both endpoints accept tool in addition to feature_id, action, event type,
text and time filters. The tool predicate is server-owned and applied before the
result limit. For example, tool=workloads_machines includes attestation and
workload issuance, not just events containing the word “ssh.” Unknown tools fail
with HTTP 400. Every export encoding preserves the same scope and retained-prefix
chain. See the CLI filter examples.
The native audit feed's trstctl:audit events can enter the packaged
Splunk app. It supplies two disabled searches for
renewal-failure and revocation bursts. Configure an exact index and tenant scope
and restricted Splunk role permissions before enabling them. Both searches count
immutable event IDs once across feed retries. The owned scaffold proof covers
ad hoc query contracts and real product outbox delivery; vendor search-engine,
scheduler and role qualification remain unverified.
Notifications (F29)
When something matters — a certificate nearing expiry, a CT-log anomaly — trstctl alerts
the right channel using reliable, journaled delivery: the alert intent is written in the
same transaction as the triggering change so a crash can't drop it, and a dispatcher
fans it to every configured channel, retrying at-least-once on failure.
Channels (full list under Reference) each satisfy one small interface and
pass a conformance check; channel secrets (webhook URLs, routing keys, API tokens) are
held in wipeable memory where the provider allows it and never logged.
HTTP-based channels default to the shared SSRF-safe client and accept only public HTTPS
endpoints, so an operator callback can't turn the control plane into a request to
loopback, RFC1918, or cloud-metadata addresses.
Tenant channel create and replace require HTTPS. Literal private receivers also
require a deployment-operator grant for that exact tenant in
notifications.tenant_private_egress; no channel request can grant itself access.
The delivery worker reuses the same grant and checks DNS at every dial. Loopback,
link-local and metadata ranges stay unavailable to tenant channels; redirects
cannot change origin, TLS verification remains required, and air-gap policy also
applies. With no grant, channels retain public-HTTPS-only delivery.
The served private-TLS test drives a real outbox delivery to an owned private
receiver and refuses another tenant's dial; console and cold-restart qualification
are separate running-image proofs.
The /notifications console names this journey Alerts and delivery. Its opening
summary says whether any ready channel and routing rule actually connect an event to a
person or system, and it separates failed-delivery evidence from configuration. One
Add channel action opens a bounded form; channel coverage, routing, and delivery
attempts remain in three closed detail sections until needed. The server applies one
fixed alert envelope across channel providers. There is no tenant-editable template
library in this build, and the console states that limitation instead of presenting a
false template editor. The console does not prefill invented channel or owner values,
and it keeps routing-policy save disabled until the server has reviewed the exact
unsaved draft and every entered destination is a ready channel. Review exact route
does not save the rule, write an event, queue a message, or contact a provider. It
returns the normalized scope and channel set, a stable request fingerprint, missing
channel blockers, the later execution effects, the recovery steps, and the checks an
operator can use after saving. Editing any field invalidates that review, so an old
green result cannot authorize a changed rule. Save also rechecks current server-side
channel readiness, closing the gap where a channel is disabled after review.
Status: served. When the lifecycle alert window is set, the leader scheduler writes
notification.expiry outbox work, stamps the certificate alerted, and includes the
owner/contact plus active approver recipients. Tenants manage channels at
/api/v1/notification-channels (create, read, replace, delete); rows store endpoint
metadata and credential references only, redact those in responses, and rebuild from
notification.channel.* events. The outbox dispatcher resolves enabled tenant-authored
channels at delivery time into their family's own transport (Slack and Teams incoming
webhooks, PagerDuty Events v2 and OpsGenie with the key the row's credential_ref
names in the tenant's secret store, an HMAC-signed webhook, a bearer-authenticated
SIEM event), opening the credential only for that delivery; email and SMS remain
operator-configured because a tenant row cannot carry their SMTP or gateway settings.
Process-configured channels remain supported for bootstrap.
Severity and threshold-day metadata route through the
severity-to-channel routing matrix instead of fanning every alert to every channel;
EffectiveAlertChannels resolves the policy-specific set at dispatch time, and a
per-(subject, threshold, channel) dedup ledger stops the same threshold from firing
twice on the same channel. Operators can list channel families, queue redacted tests at
/api/v1/notification-channels/{id}/test, work the inbox (list, get, mark read), and
requeue failed dispatches from /api/v1/notifications/{id}/requeue. Every successful
fan-out records a notification.delivery.recorded event before the outbox row is acknowledged,
so a retry after failure skips channels already rebuilt from the event log; channel
tests replay through notification.test.queued events. Operators can review an exact
routing-policy draft at POST /api/v1/notification-routing-policies/preview, manage
the saved rules at /api/v1/notification-routing-policies, and inspect the effective
saved hierarchy with GET /api/v1/notification-routing-preview. Recovery is explicit:
replace or delete a bad policy, then requeue only the failed delivery. Retrying is safe
because completed channel receipts are preserved and skipped.
Compliance reporting (F62)
Compliance reporting turns the audit log and the CBOM into
signed, reproducible evidence packs covering all 15 supported frameworks (full list
under Reference). A v5 pack binds its claims to one tenant and one
inclusive 90-day evidence window. Every evidenced control names exact immutable event
references (ID, tenant-local sequence, type, time, and audit-chain digest) or exact
tenant graph objects. Every prerequisite must resolve inside that boundary; missing,
stale, malformed, or wrong-tenant evidence is a visible gap. CNSA 2.0's PQC control,
for example, passes only when post-quantum assets exist and quantum-vulnerable ones do
not. Reports use a persistent compliance-evidence key in the isolated signer.
Its public key must be pinned independently for offline verification; a valid
signature proves authenticity of the bounded manifest, not certification or
auditor sufficiency.
Each pack states residuals honestly. fips-140 marks POST evidenced only when the
running module is active and its fail-closed self-test passes; build provenance,
crypto-boundary artifacts, CMVP certificate, and deployment configuration stay gaps
without exact evidence. common-criteria can evidence attributable policy/change and
credential-lifecycle facts, while its security target and lab evaluation remain gaps.
cabf-br requires exact profile, CA issuance/revocation, custody, and ceremony events;
CP/CPS publication and independent public-trust audit remain external. soc2 requires
current inventory plus complete ownership and review for CC6, policy/lifecycle/
monitoring events for CC7, and active-policy/approved-change/lifecycle evidence for
CC8; scope, management assertion, sampling, and the independent CPA report remain
residuals.
Status: served — evidence-pack export, inventory/schedule surface, and NHI
compliance mapping below are all live REST/CLI/console routes. GET /api/v1/compliance/inventory-report enumerates served frameworks, report types, backing
routes, evidence references, schedule rows, and inventory counts across certificates,
CBOM assets, and discovery/report schedules; POST/GET /api/v1/compliance/report-schedules record and list idempotent, event-sourced
schedules. With the licensed governance signer and a private mounted
audit.archive_dir, the leader now generates one signed local artifact per due
edge and exposes event-backed run receipts, exact downloads, bounded retry,
dead-letter and requeue through /api/v1/compliance/report-runs. audit_export
means the local archive stays the record. A schedule's optional dispatch announces
each completed run: a compliance.report_ready notice on each named notification
channel (up to 8, each configured for the tenant) and one ServiceNow ticket on an
operator-approved public binding, queued with the completion and named on the run
receipt. A notice carries the run, its SHA-256 and its download route, never the report
body. A person can hold a completed run's report (POST|DELETE /api/v1/compliance/report-runs/{id}/legal-hold, with a reason), and the opt-in
audit.report_retention removes unheld reports older than its window, recording a run
expired only once a read of the archive finds the report gone.
POST /api/v1/compliance/report-schedules/preview validates and
fingerprints the exact unsaved definition without appending an event, projecting a row,
generating a report, or calling a destination. The console invalidates that review after
any edit and keeps Create schedule disabled until the exact draft is ready. Saved
schedules expose named /{id}/pause and /{id}/resume recovery routes. Pause retains
the definition and prior evidence; resume calculates a fresh full interval. Both actions
are tenant-scoped, permission-checked, event-sourced, and idempotent. This limitation
does not apply to the separate native audit
feed: PUT /api/v1/audit/feeds/{id} and GET /api/v1/audit/feeds configure and
observe durable Splunk HEC or Microsoft Sentinel batches. GET /api/v1/compliance/nhi-report builds a separate mapping (NIST SP 800-53 Rev. 5, NIST
CSF 2.0, PCI DSS 4.0, DORA, ISO/IEC 27001:2022 Annex A, FedRAMP, CMMC 2.0, eIDAS, NIS2)
from served NHI posture only — never documentation or unsupported claims — with finding
counts and residual attestations for legal scope, control applicability, and auditor
sampling.
The same surface runs NHI access certification campaigns: a reviewer starts a campaign
with non-secret NHI/resource/entitlement items, then records each item decision as
certified, revoked, or exception — event-sourced, with routes and events listed
under Reference. The request body accepts identifiers and evidence refs
only; inline secrets and credential values are rejected.
Credential assurance records (A179)
GET /api/v1/certificates/{id}/assurance-record (certs:read; CLI trstctl certificates assurance-record <id>) returns one certificate's lifecycle as a single
signed record (format trstctl.credential-assurance-record/v1). It joins every
stage with its evidence ID:
- discovery: each discovery finding of the certificate's fingerprint (source, location, time), or the inventory's last observation when no finding remains;
- owner and issuance: the issuing CA, the issuance event (or the transition that decided a lifecycle issuance or renewal), the profile revision and approval that command carried, and the key origin;
- deployments: every connector delivery receipt for the certificate;
- verifications: verification receipts — marked
signed_observationwhen the agent or relay that reported them signed a job receipt the server verified — and the latest endpoint observation per listener and vantage (local or relay): the served fingerprint, which comparisons ran, any mismatch, and the signed transcript digest; - external observations: what external observers reported seeing at its endpoints, and the verdict against what trstctl deployed there, listed apart from trstctl's own verification (see External observers);
- renewals: the run that produced it or replaced it, and the certificates on either side; revocation; and the tenant audit entries that name it, with their chain hashes.
Each stage is classified: independently_observed when evidence came from outside
trstctl's own record (a discovery scan, an endpoint seen serving this certificate),
recorded_only when only trstctl's record of its own action stands behind it, and
absent when there is none. A delivery counts as observed only when an endpoint was
then seen serving the certificate, by trstctl's own verification or an external
observer's matching report.
The response carries the record, a compact JWS over its exact bytes (artifact domain
trstctl.audit-evidence/credential-assurance-record/v1, signed by the isolated
signer's audit-export key), its SHA-256, the signer's public JWK set, and a
Markdown rendering. trstctl certificates verify-assurance-record --record <file> --audit-jwks <pinned file> checks a saved record offline: the signature and its
domain against a JWK set you pinned separately (the file's own copy is never
trusted), and that the record beside the signature is exactly the signed one; a
changed value fails. Compliance evidence packs carry the records of the
certificates whose lifecycle changed inside the pack's window (up to 50, newest
first; credential_assurance_truncated says when there were more), under the pack's
own signature.
Privacy and data-subject controls (F79)
Privacy controls are first-class governance surfaces, not hidden compliance helpers.
Before either destructive workflow, POST /api/v1/privacy/subject-erasures/preview and POST /api/v1/privacy/retention-runs/preview perform effect-free, tenant-scoped reads.
Subject review returns exact current per-class counts, archive-disposition and
active-legal-hold evidence, later writes, a tenant-bound fingerprint, and explicit
recovery/verification instructions. Retention review resolves the effective tenant
policy and returns the current cutoffs and eligible counts. Neither preview appends an
event, projects a row, reserves an idempotency key, or calls an outside system.
POST /api/v1/privacy/subject-erasures records a tenant-scoped subject erasure, emits
privacy.subject.erased, and projects pseudonymized or cleared personal data while
keeping audit evidence verifiable. POST /api/v1/privacy/retention-runs records a
non-audit PII retention pass, emits privacy.retention.enforced, and applies configured
retention windows to operational metadata; later runs do not count already-pseudonymized
rows as fresh work. POST /api/v1/privacy/subject-exports
answers access/portability requests as a read-only export that does not mutate state or
carry an Idempotency-Key. GET /api/v1/privacy/catalog exposes the maintained
personal-data catalog so operators can see what fields are subject to erasure and
retention.
Erasure binds the authenticated caller, exact route, subject, and reason to its
Idempotency-Key. A tenant-scoped durable operation record retains the canonical
event identity and response after the generic response cache expires or the live
event moves to the signed audit archive. That record is independent PostgreSQL
backup state, so projection rebuild and snapshot restore do not erase retry
authority.
The erasure projection revokes the subject's browser sessions and records an
offboarded member tombstone. A role change claimed before erasure cannot be
published afterward; an already-retained role event is ordered by source
sequence and cannot restore the erased member's authority. A new explicit
rehire may create a fresh member record and session without reviving old
sessions.
When erasure changes bytes in the hot event history, the control plane also emits
history.tenant_data_rewrite.continuity. This is system-signed generation evidence,
not a second operator API event: its canonical JWS binds the tenant, old/new
generation and configuration identities, invariant/mapping/content roots, audit-chain
heads, retention seed, and the disclosure that older external archives, exports, or
backups may still retain the source bytes.
The CLI exposes the same controls through privacy erasures preview, privacy erasures erase, privacy erasures list, privacy retention preview,
privacy retention run, privacy retention list, privacy export, and privacy catalog; the web console's /privacy screen covers erasure, retention enforcement,
retention evidence, and the catalog. See Privacy data catalog
for the row-level map and erasure/retention behavior.
In the console
The /policy screen is named Rules and approvals. Its opening answer says whether
the fail-closed protection gate is on, whether a custom rule is active, how many rule
versions are recorded, and how many access requests need approval. It never reports a
healthy state when either opening API cannot be verified. The only opening action,
Create rule, saves a reviewed draft; it does not activate that draft.
Exact machinery remains available in four closed sections: rule versions and change
history, safe rule testing, framework evidence and reports, and approval/access
reviews. Opening framework evidence loads any of the 15 supported frameworks (list
under Reference), its signed tenant and coverage window, exact
per-control references, and missing prerequisites. The signed manifest also counts
certificate custody by origin, storage, and exportability and lists every incomplete
certificate with its missing fields; the screen shows those exact gaps instead of
hiding them in a percentage. The reporting section also contains the compliance
inventory report, audit-export schedules, and NHI compliance mapping; a schedule's
report type ranges from a framework evidence pack to the cryptographic risk report
(weak, distrusted and quantum-vulnerable keys with their deadlines and plans, see
Lifecycle and PQC) and the
customer assurance report (the tenant's credential assurance in plain sentences and
its white-label brand, for a Provider to share, see
Compliance). The safe-test
section calls POST /api/v1/policy/dry-run to compile a candidate lifecycle or ABAC
Rego module against tenant input, returns allow/deny/error plus a bounded trace, and
appends policy.dry_run.evaluated without raw values. The change-history section
serves lifecycle rule listing, activation, and rollback through
/api/v1/policy/versions; activation remains a separate recorded action after the
candidate compiles. The approvals section contains NHI access-certification campaigns
and access-change decisions. The /audit
screen is a filterable audit explorer (type presets such as Policy decisions, time and
sequence windows) that downloads a signed evidence bundle and manages scheduled
Splunk HEC/Sentinel delivery with cursor, record lag, retry, failure, and collector
receipt state; the /privacy screen renders
the served data-subject controls and catalog. See The web console.
Use it
The audit log is served — query it and export evidence:
# query the tamper-evident log
trstctl-cli audit events --type policy.decision --since 2026-01-01T00:00:00Z --limit 100
# download a signed evidence bundle for a date range
trstctl-cli audit export --since 2026-01-01T00:00:00Z --until 2026-06-01T00:00:00Z
# review, configure, and observe a native collector feed (body shown in docs/cli.md)
trstctl-cli audit feeds preview <feed-uuid> -f audit-feed.json
trstctl-cli --idempotency-key audit-feed-production audit feeds set <feed-uuid> -f audit-feed.json
trstctl-cli audit feeds list
# export a signed SOC 2 evidence pack
trstctl-cli compliance evidence-pack soc2
# read compliance inventory coverage
trstctl-cli compliance inventory-report
# read NHI compliance mappings
trstctl-cli compliance nhi-report
# dry-run a candidate Rego module, then author/activate a lifecycle policy version
trstctl-cli policy dry-run --body policy-dry-run.json
trstctl-cli --idempotency-key policy-author-42 policy versions create -f lifecycle-policy-version.json
printf '{"reason":"CAB approved"}' | trstctl-cli --idempotency-key policy-activate-42 policy versions activate <version-id> -f -
# record a report-schedule definition; dispatch is optional and announces each
# completed run on the named channels and in one ServiceNow ticket
cat > soc2-schedule.json <<'JSON'
{"framework":"soc2","name":"weekly-soc2-pack","report_type":"framework_evidence_pack",
"interval_seconds":604800,"delivery":"audit_export",
"dispatch":{"channels":["email","webhook"],
"ticket":{"provider":"servicenow","instance_url":"https://example.service-now.com",
"table":"sc_task","token_ref":"secret://itsm/servicenow"}}}
JSON
trstctl-cli compliance report-schedules preview -f soc2-schedule.json
trstctl-cli --idempotency-key weekly-soc2 compliance report-schedules create -f soc2-schedule.json
trstctl-cli --idempotency-key pause-weekly-soc2 compliance report-schedules pause SCHEDULE_ID
trstctl-cli --idempotency-key resume-weekly-soc2 compliance report-schedules resume SCHEDULE_ID
trstctl-cli compliance report-schedules runs SCHEDULE_ID
trstctl-cli compliance report-runs get RUN_ID
trstctl-cli compliance report-runs download RUN_ID > signed-report.json
trstctl-cli --idempotency-key retry-run-1 compliance report-runs requeue FAILED_RUN_ID
trstctl-cli --idempotency-key hold-run-1 compliance report-runs legal-hold RUN_ID -f hold.json
trstctl-cli --idempotency-key release-run-1 compliance report-runs release-hold RUN_ID
# start and decide an NHI access certification campaign
# The start request assigns a reviewer distinct from this authenticated creator.
trstctl-cli access reviews start -f nhi-review.json
# Sign in as that assigned reviewer before making the decision.
trstctl-cli access reviews decide <campaign-id> <item-id> -f nhi-review-decision.json
# file and review data-subject privacy controls
trstctl-cli privacy erasures erase -f subject-erasure.json
trstctl-cli privacy retention run
trstctl-cli privacy catalog
Routes for every command above are in Reference; all mutations need an
Idempotency-Key. Evidence packs support all 15 frameworks (path values in
Reference) and return a signed export plus public_key_der so an auditor
can verify the manifest offline. RBAC is enforced automatically. A default-deny policy
looks like this in Rego:
package trstctl.policy
default allow = false
allow { input.action == "revoke" }
allow { input.action == "issue"; input.profile != "" }
Turn on the ABAC deny overlay when a decision depends on current deployment state or resource tags:
auth:
abac:
enabled: true
environment:
change_window: "false"
module: |
package trstctl.abac
default deny := false
deny if {
input.permission == "certs:issue"
input.resource.env == "prod"
input.env.change_window != "true"
}
Pitfalls & limits
- Read the status line in each section above: every capability here is served, but
several are gated behind a config flag (
ca.policy.enabled,ca.policy.require_approval,auth.abac.enabled); Reference gives the exact flag and route. - Policy fails closed. If your Rego is wrong or the engine is overloaded, operations are denied, not allowed. Test policy changes before rollout.
- Compliance reporting, privacy controls, NHI campaigns, and access-change approvals evidence controls; they do not certify you. Campaigns prove a reviewer attested to listed access at a point in time; approvals prove who approved a scoped NHI change against a PR/ticket/CAB reference; privacy evidence proves the product executed the configured controls. External auditors decide whether your whole program meets a framework — see also Audit & compliance.
- Notifications are at-least-once, so design channel handlers to tolerate a duplicate.
- An assurance record states what trstctl holds about a certificate; it does not observe anything new. A certificate no scan found and no endpoint was seen serving has no independently observed stage, and a scheduler renewal that bound no profile says so instead of naming one. See Current limitations.
Reference
- Policy:
Engine.Evaluate(Input{Action, Profile, Actor, TenantID, Attrs}); actionsissue,deploy,revoke; fail-closed;policy.decisionevents; dry-run and versioning atPOST /api/v1/policy/dry-run,POST|GET /api/v1/policy/versions,POST /api/v1/policy/versions/{id}/activate, andPOST /api/v1/policy/versions/{id}/rollback. - ABAC deny overlay:
package trstctl.abac;input.permission,input.resource.*,input.env.*,input.now_hour_utc; deny-only; fail-closed;policy.abac.decisionevents. - RBAC: permissions
<resource>:<verb>; rolesadmin,operator,viewer,auditor,ra-officer;guardmiddleware. - Audit (served):
GET /api/v1/audit/events(type,since,until,as_of,q,limit),GET /api/v1/audit/export,GET /api/v1/audit/feeds, andPUT /api/v1/audit/feeds/{id};Seal/VerifyChain. Feed configuration, queued exact batches, delivery receipts, and terminal failures are immutable events; the external call occurs only from theaudit.feed.*outbox worker. - Compliance reporting (served):
GET /api/v1/compliance/evidence-packs/{framework},GET /api/v1/compliance/inventory-report,GET /api/v1/compliance/nhi-report,POST|GET /api/v1/compliance/report-schedules, schedule run receipts and exact signed artifacts under/api/v1/compliance/report-runs; the localaudit_exportarchive is the record, and a schedule'sdispatchannounces each completed run on named notification channels and in one ServiceNow ticket. Legal holds and the opt-inaudit.report_retentiondecide how long each report stays. - Access-change approvals (served):
POST|GET /api/v1/access/requests[/{id}], andPOST /api/v1/access/requests/{id}/decisions; CLI:access requests create|list|get|decide. - Notifications: email, Slack, Teams, SMS, SIEM, PagerDuty, OpsGenie, webhook
(HMAC-signed); HTTP targets are public HTTPS by default; channel routes are
POST|GET /api/v1/notification-channels,GET|PUT|DELETE /api/v1/notification-channels/{id}, andPOST /api/v1/notification-channels/{id}/test; routing-policy routes arePOST /api/v1/notification-routing-policies/preview,POST|GET /api/v1/notification-routing-policies,GET|PUT|DELETE /api/v1/notification-routing-policies/{id}, andGET /api/v1/notification-routing-preview; inbox routes areGET /api/v1/notifications[/{id}],POST /api/v1/notifications/{id}/read, andPOST /api/v1/notifications/{id}/requeue. - Compliance frameworks (15, evidence packs): PCI-DSS (
pci-dss), HIPAA (hipaa), SOC 2 (soc2), NIST SP 800-53 (nist-800-53), NIST CSF 2.0 (nist-csf-2.0), FedRAMP (fedramp), CMMC 2.0 (cmmc-2.0), CNSA 2.0 (cnsa-2.0), FIPS 140 (fips-140), Common Criteria (common-criteria), CA/Browser Forum Baseline Requirements (cabf-br), WebTrust (webtrust), ETSI (etsi), eIDAS (eidas), and NIS2 (nis2). - NHI access reviews:
POST|GET /api/v1/access/reviews[/{id}],POST /api/v1/access/reviews/{id}/items/{item_id}/decision; decisionscertified,revoked,exception; eventsnhi.access_review.campaign.startedandnhi.access_review.item.decided. The console asks the creator to assign a different authenticated reviewer withreviewer_subject. API and CLI callers may omit that optional field; the campaign then enters the expliciturn:trstctl:access-review:any-distinctreviewer queue. A different authenticated operator withaccess:writecan decide a queued item, while the creator cannot. For an explicitly assigned campaign, only that reviewer can decide. Supplying the queue marker as a reviewer subject is refused; only omission selects this mode. The server records the authenticated subject asdecision_by; a supplied decisionreviewer_subjectmust match it. Rejected decisions emit no decision event. Sign in as the assigned reviewer, or as a different authorized operator for an unassigned campaign, before using the API, CLI, or console to certify, revoke, or except an item. Certification may omit a reason; revoke and exception decisions require one. The console accepts separate decision evidence references and shows them after recording; it does not reuse the campaign's evidence references as the reviewer's assertion. Its campaign form requires a reviewer other than the signed-in creator. A viewer who is not the assigned reviewer sees that identity and cannot use the decision controls. In the unassigned queue, the console names that mode and enables controls only for a signed-in operator other than the creator. The API enforces both rules before event append. During cold recovery, the projector keeps the first valid decision for each item and excludes older duplicate or actor-mismatched decision events from the read model. The events remain in the append-only audit history. An operator rebuilding a read model that was projected by an older build must use the normal projection rebuild path to apply these recovery rules. - Privacy controls:
POST|GET /api/v1/privacy/subject-erasures,POST|GET /api/v1/privacy/retention-runs,POST /api/v1/privacy/subject-exports,GET /api/v1/privacy/catalog; operator-command eventsprivacy.subject.erasedandprivacy.retention.enforced, plus system-signed generation evidencehistory.tenant_data_rewrite.continuitywhen hot history bytes change.
See also
Platform & API (where RBAC is enforced) · Workload identity (the policy gate in action) · Observability & risk (the CBOM behind compliance) · Audit & compliance · Product threat model · glossary: event sourcing, bulkhead, idempotency
Covers: F28, F29, F62, F79, F8, F9
Certificate CI export
Publish a certificate's public metadata to a ServiceNow certificate CI through a separate write binding. Ownership reconciliation keeps its own read-only relay workflow and never inherits that permission.
An operator configures a per-tenant itsm.servicenow.certificate_exports binding.
The authenticated export route records an immutable command and an outbox intent
in the same SQL transaction. The worker calls IRE for cmdb_ci_certificate, then
reads the exact returned CI to verify all exported fields before recording a
receipt. Retries keep the source identity, and a retained receipt settles a retry
without another write. Credentials are leased as locked bytes only for delivery.
Use the Connect other tools console or trstctl-cli cmdb certificate-exports create|show; the API paths are /api/v1/integrations/cmdb/certificate-exports
and its /{id} read. A queue acknowledgment stays pending until readback matches.
Later lifecycle changes require another explicit export. The served TLS stand-in
proof is TestServedCertificateExportWritesOnlyItsOwnCMDBBindingAndReadsBack;
SQL rollback recovery is TestCMDBExportRecoversAnAppendWhoseOutboxTransactionRolledBack.
Live ServiceNow class/source/ACL behavior needs an instance. See
configuration for
prerequisites, field semantics and failure recovery.
Named ticket intake and ownership
Configure several ServiceNow tables or Jira projects under distinct schedule names.
Each source keeps its own tenant-scoped relay job and bounded page cursor. API,
CLI and the Approvals console list and edit those schedules; removal refuses an
unfinished sweep or pending job and preserves the request and audit history.
Omitted names remain bound to the legacy default source.
An optional owner_field reads an exact email and resolves it to one existing
tenant owner. Hidden, missing, ambiguous and foreign mailboxes leave a request
unassigned, visibly and without guessing. Existing requests keep their recorded
owner; a new ticket/request is needed to correct an unassigned one. The existing
request lifecycle carries a mapped OwnerID into identity preparation and issuance.
No approval, ownership-readiness or requester separation check is bypassed.
TestServedNamedTicketIntakesKeepTheirSourcesAndMapTenantOwners covers two tables
and two Jira projects with independent jobs, owner mapping and cross-schedule
report refusal. The named lifecycle test covers removal idempotency, active-sweep
and permission refusal, and ambiguous/foreign-owner boundaries. Migration content
checks preserve the old default cursor. These served observations and protocol
fixtures still owe final-image relay/provider replay; live vendor accounts are
separate qualification inputs. See configuration.