Platform & API — how you drive trstctl, and how it runs
What it is
This page covers the platform plumbing — the surfaces you use to operate trstctl and the properties of how it runs: the REST API, the CLI, the web UI, OIDC/SAML/LDAP sign-on, SCIM 2.0 provisioning, RBAC plus ABAC authorization, single-binary distribution, encrypted transport, multi-tenant topology, and federation. Not glamorous, but this is what makes trstctl usable, secure, and operable in a real organization.
The mental model: if the feature pages are the appliances, this is the wiring, the breaker box, the front door lock, and the meter — the infrastructure that lets everything else run safely on shared premises.
Why it exists
A control plane is only as good as how you interact with it and how safely it runs. You need a programmable API for automation, a CLI for scripts and CI, a UI for humans, SSO so people log in with existing accounts, encrypted channels so nothing is sniffable, hard tenant isolation so customers can't see each other, and a distribution simple enough to actually deploy. These are the table-stakes properties an enterprise checks before it trusts a platform with its credentials.
How it works
The REST API (F10)
The API is data-driven: every route is declared once in a single registry that
simultaneously generates the served http.ServeMux, the OpenAPI 3.1 document (served
at /api/v1/openapi.json), the CLI command table, and the Terraform provider route
constants for its managed resources — so the spec, the server, the CLI, and the
infrastructure-as-code surface can't drift apart. Errors are RFC 7807
application/problem+json. Every mutation
requires an Idempotency-Key, recorded in PostgreSQL so a retry returns
the original result instead of applying the change twice. Tenant comes from the
authenticated principal (so one tenant can never act on another's data), pagination uses
opaque cursors, and over-budget callers get 429 with Retry-After. Every guarded
route checks RBAC first; when auth.abac.enabled is configured, a deny-only ABAC
overlay can then block the request using route, actor, environment, and time attributes.
Served.
The CAP-API-07 receipt: the served POST /api/v1/identities/{id}/transitions
route requires an Idempotency-Key, appends the lifecycle event once, and binds the
matching ca.issue outbox row to that same key so a retry returns the first result
instead of minting or dispatching twice. Webhook eventing uses the same spine:
operators author channels at /api/v1/notification-channels, test a channel through
POST /api/v1/notification-channels/{id}/test, and expiry or response alerts enqueue
notification outbox work before the dispatcher delivers the signed webhook.
The CLI (F11)
trstctl-cli is the API's twin: every command is a row in a table that maps
trstctl-cli <group> <verb> straight to an API route, so the CLI is provably at parity
with the API and carries no bespoke logic. It auto-supplies idempotency keys on mutations.
The command groups span the full route surface — from owners, identities,
certificates, and secrets through discovery, nhi, breakglass,
graph, risk, managed-keys, transit, ai, and mcp — see the
CLI reference for the complete table. Served (binary).
Terraform provider
terraform-provider-trstctl is built with the standard HashiCorp Terraform plugin
framework and ships as a normal trstctl binary. It manages certificate profiles through
POST /api/v1/profiles, issues short-lived certificates through
POST /api/v1/secrets/pki, and manages application secrets through
/api/v1/secrets/store. Each mutation sends an Idempotency-Key, uses the same bearer
token and tenant headers as the CLI, and is backed by provider tests plus a real
Terraform apply acceptance test. See Terraform provider.
Served (binary).
The web UI (F12)
The UI is a React 18 + Vite + shadcn/ui single-page app served by the binary from
an embedded filesystem on the same port and TLS certificate as the API. Real hashed
asset files are served directly, deep links fall back to the SPA, and /api/* stays
owned by the API handler. No separate static server is required. Served. The embedded
index.html references the real Vite bundle, and tests fail if a clean build regresses to
the placeholder.
The console is organized as a space rail over scoped sidebars — the
Certificates & PKI, Secrets, Workload & SSH, Posture & response, and
Platform spaces each own their product surfaces, with Home carrying the
cross-space dashboard, journeys, and worklists — and every served capability
across these feature pages has a screen behind it: the certificate command center, the
secrets workspace, non-human-identity governance, discovery, the PQC posture gauge, the
compliance and audit surfaces, the privacy (/privacy) governance console, and the
integrate (/integrate) hub that hands you copyable ACME / EST / SCEP enrollment URLs,
the language SDKs, and the Terraform / cert-manager / SPIRE integrations. Navigation is
RBAC-gated and one row per route, and every label resolves through the typed i18n catalog.
The full route-to-screen map is The web console. Served.
OIDC, SAML, and LDAP / Active Directory sign-on (F13)
People log in through OIDC (OpenID Connect), SAML 2.0, or LDAP / Active
Directory against a standards-compliant provider. OIDC uses the authorization-code flow with random state
(CSRF protection) and a mandatory nonce (replay protection); the returned id_token is
verified — signature (via JWKS through the single isolated cryptography path), issuer,
audience, expiry, nonce — before session issue. SAML serves a Service Provider at
/auth/saml/metadata, starts SP-initiated login at /auth/saml/login, and accepts
signed POST-binding assertions at /auth/saml/acs; IdP metadata and XML signature
verification stay behind the same isolated cryptography boundary. LDAP / Active
Directory mounts POST /auth/ldap/login, binds the user to the directory, searches
directory groups, and maps those groups to tenant roles. All three paths mint the same
short-lived, HMAC-signed, HttpOnly+Secure session cookie and resolve the verified
subject, tenant claim, or groups through the same per-user tenant-mapping table. CI/CD
instead uses API tokens (trst_-prefixed, only the SHA-256 hash stored). Served when
auth.oidc.enabled, auth.saml.enabled, or auth.ldap.enabled is configured. API
tokens remain the zero-dependency auth path when SSO is disabled; an
enabled-but-incomplete OIDC, SAML, or LDAP block fails closed at startup.
Operators can start from the per-IdP OIDC runbooks for
Keycloak, Authentik, Okta, Auth0, Microsoft Entra ID, and Google Workspace.
Those pages use the real TRSTCTL_AUTH_OIDC_* settings, including PKCE S256,
authorization response iss validation, back-channel logout, and the encrypted
tenant-scoped credential-store reference for confidential-client secrets.
SCIM 2.0 provisioning
Directory provisioning is served under /scim/v2 when auth.scim.enabled is on.
An IdP such as Okta or Microsoft Entra sends a tenant-bound bearer token to
/scim/v2/Users and /scim/v2/Groups; trstctl hashes the configured token file at
startup and keeps only the hash in memory. The token chooses the tenant before any
payload is read, so a SCIM request cannot smuggle a tenant id in its JSON body.
SCIM users project into the same tenant-member read model used by RBAC. Creating or
updating a user appends a tenant-member upsert event; active:false or DELETE appends
an offboarding event. SCIM groups map to existing RBAC role names: a group named
viewer gives its members the viewer role, and removing a member removes that role.
Browser sessions consult the current tenant-member roles on each API request, so
SCIM provisioning and deprovisioning change real authorization, not just an admin list.
Supported IdP operations are SCIM Users create/get/list/put/patch/delete and Groups
create/get/list/patch/delete. Served when auth.scim.enabled is configured.
Single-binary distribution (F14)
For evaluation, the one trstctl binary can supervise its own datastores: bundled
PostgreSQL (downloaded once, checksum-pinned, run on loopback) and embedded,
file-backed NATS JetStream. Bundled PostgreSQL is allowed only for host archives with
committed runtime pins in deploy/supply-chain/embedded-postgres.json (summarized in
Supply chain) (linux-amd64, linux-arm64v8, darwin-arm64v8
today), and startup fails closed if
the archive is unsupported, unpinned, or hash-mismatched. Even bundled, Postgres runs
under the non-superuser trstctl_app role so row-level security still applies — per-tenant
isolation is enforced at the database layer even for eval, not relaxed. The
signing service is always a separate supervised child
process, never in-process — private-key operations stay in their own isolated service. For
production, flip Postgres/NATS to external. Served (binary).
The CAP-MODEL-01 receipt (Self-hostable, run-anywhere): the served
GET /api/v1/platform/distribution route and trstctl-cli platform distribution
command return the self-hostable run-anywhere posture directly from the product. The
receipt lists the host-archive eval path, Docker Compose eval path, Kubernetes/Helm
path, and external-datastore production path; supported host archives
(linux-amd64, linux-arm64v8, darwin-arm64v8); the embedded-PostgreSQL runtime
pin; release gates such as make lint test, embedded-Postgres scan receipts,
OpenAPI/CLI route parity, and the architecture linter; and the open-core guard that
offline license verification plus audit/export stay in core.
The CAP-MODEL-03 receipt (Air-gapped / on-prem + data residency): the same served
GET /api/v1/platform/distribution route and trstctl-cli platform distribution
command return an air_gap receipt. It lists TRSTCTL_AIRGAP_ENABLED,
TRSTCTL_AIRGAP_ALLOW_PRIVATE, TRSTCTL_AIRGAP_ALLOW_HOSTS,
TRSTCTL_AIRGAP_ALLOW_CIDRS, values-airgap.yaml, operator-owned PostgreSQL and
NATS endpoints, and the offline transfer bundle with checksums. The receipt also
points to docs/airgap.md, scripts/airgap-bundle.sh, the Helm air-gap overlay,
and internal/server/airgap_served_test.go, whose served integration path proves
certificate issuance plus native-secret create/rotate with zero public egress after
a synthetic public-endpoint tripwire proves the runtime guard is armed.
Encrypted control-plane transport (F15)
Every channel is encrypted. By default the signing service is reached over a Unix
domain socket with SO_PEERCRED peer-uid authentication (a 0600 socket; a different
uid is rejected); across nodes it is reached over mTLS (TLS 1.3, AEAD-only, the
control plane and signer each pinning the other's certificate). Either way the signing
service has no HTTP server and no SQL driver — it stays a separate, isolated process — and
at startup it disables core dumps and ptrace so secret material can't be read out of its
memory. The REST API/UI is served over TLS (self-signed by default for instant start,
operator cert in production, TLS 1.3, AEAD-only). Agents connect over
mTLS with short-lived, auto-rotated client certificates. The
KMIP listener, when enabled, is also TLS 1.3 mutual TLS and verifies client
certificates against TRSTCTL_PROTOCOLS_KMIP_CLIENT_CA_FILE. All TLS/x509 code lives
behind the single isolated cryptography path. Served.
Note: the default signer channel is a peer-authenticated UDS (co-located/sidecar); the cross-node channel is mutually-authenticated, mutually-pinned mTLS.
Multi-tenant topology (F40 / CAP-IAM-02)
Isolation between tenants is enforced by PostgreSQL itself, not by application code —
one tenant can never read another's data, and that guarantee lives at the database layer.
Every table carries a tenant_id and has row-level security that denies
all rows when the tenant context is unset (fail-closed). WithTenant drops to the
non-superuser role and sets the tenant for the transaction, so every query is confined
automatically — and a custom build check fails the build if any repository query omits
the tenant filter. A single-company deployment simply runs one tenant.
Managed offering / SaaS provider plane (CAP-MODEL-02)
trstctl also serves the provider-plane path needed for a managed or SaaS offering. A
Provider-tier license enables provider_plane; without it, the status endpoint is still
readable but tenant provisioning fails with 403. Provider operators can inspect posture
with GET /api/v1/managed-offering/status or trstctl-cli managed-offering status, then
create a hosted tenant with:
trstctl-cli managed-offering tenants provision -f hosted-tenant.json
hosted-tenant.json carries only non-secret topology facts:
{
"tenant_id": "33333333-3333-4333-8333-333333333333",
"name": "Acme Hosted",
"region": "us-east-1",
"data_residency": "US",
"plan": "enterprise",
"support_tier": "24x7",
"slo_tier": "99.95"
}
The mutation is idempotent like every other state change. It emits
tenant.registered for the hosted tenant and includes managed_offering metadata in the
immutable event payload (provider_tenant_id, region, residency, plan, support tier, SLO
tier, and actor). The projector then builds the tenant row from that event, so the hosted
tenant receives its own PostgreSQL RLS boundary immediately. No managed-service token,
customer secret, or billing credential belongs in this request; those use the normal
tenant-scoped secret/API-token/session paths.
The web console's Platform page shows the same provider-plane status and offers the
tenant-provisioning form when the operator has access:write. The served acceptance test
boots the same binary composition used by production tests (PostgreSQL, NATS JetStream,
and the separate signer process) and proves the Provider license gate, tenant projection,
event metadata, and idempotent replay.
The public packaging boundary has three tiers — Free (self-hosted MPL core),
Enterprise (the commercial ee/ set, per control-plane deployment), and
Provider/MSP, which inherits Enterprise, adds provider-plane operation, and
grants managed-service and resale rights with dedicated-deployment flexibility.
The tier matrix and billing posture live in Editions and
Pricing. Every path uses the same event-sourced,
PostgreSQL-RLS-isolated binary lineage; none moves tenancy, audit/export,
crypto, or license verification into ee/.
High-volume orchestration (CAP-SCALE-01)
trstctl serves a scale-orchestration posture for 100,000 to 1,000,000+ managed
credentials. GET /api/v1/scale/orchestration and
trstctl-cli scale orchestration expose the same plan:
- credential bands for 100k, 250k, and 1M managed credentials;
- the selected
CAP-LARGEtier from the committed capacity model; - hot-path SLOs, release gates, and required measurement artifacts;
- execution lanes for issuance, inventory, graph/risk, revocation, signer, and projection replay;
- the bounded queues, bulkhead environment knobs, backpressure signals, replay source, and AN-* architecture invariant for each lane;
- the shard plan for inventory pages, CRL shards, and projection batches;
- explicit residuals for customer infrastructure pricing, external relying-party CRL adoption, and remote CI behavior.
This endpoint is read-only and guarded by access:read. It does not count a vendor SKU
or customer CDN as product-owned serving evidence. Instead it turns the already committed
performance and capacity denominator into an operator-visible execution plan tied to
scripts/perf/artifacts/smoke-baseline.json,
scripts/perf/artifacts/live-load-baseline.json, and the soak gate. The Platform page
renders the same plan with the served lane/gate/band tables.
Multi-region HA issuance (CAP-SCALE-02)
trstctl serves the regional HA issuance posture at
GET /api/v1/scale/ha-issuance and trstctl-cli scale ha-issuance. This is an
active regional ingress model, not independent split-brain CA writers: multiple regions
can accept issuance traffic, but every tenant mutation still commits through a fenced
writer plane with the same idempotency record, event append, transactional outbox,
leader election, and isolated signer boundary.
The endpoint returns:
- active issuance regions, their writable scope, signer placement, datastore/event-log posture, and health signals;
- tenant write fences for idempotency, event-log append, outbox side effects, leader-owned workers, and signer isolation;
- regional issuance lanes for API, agent renewal, and protocol enrollment traffic;
- failover steps and release gates for regional smoke, failover drills, idempotency replay, and the architecture linter;
- default RPO/RTO targets of 5 seconds and 30 seconds, plus residuals for customer DNS, ingress, datastore promotion, signer/HSM latency, and external CA provider limits.
This fits beside F41 federation rather than replacing it. Federation still gives a passive region replayable read state from peer event import. CAP-SCALE-02 adds a served operator contract for active regional issuance ingress only when the shared or promoted writer endpoint, replicated JetStream event log, idempotency table, outbox, and signer health are green. If those fences degrade, issuance fails closed instead of minting from two independent writers.
Enterprise support, SLAs, and professional services (CAP-MODEL-04)
trstctl serves an Enterprise support posture endpoint so an operator can inspect the
standard support packages without relying on static sales copy. The
GET /api/v1/support/enterprise API and trstctl-cli support enterprise return:
- the live
ha_supportlicense mode (enabled,read_only, oroff); - the business-hours and 24x7 production support tiers;
- P1, P2, and P3 response/update SLA targets;
- deployment-architecture, migration-readiness, and credential-incident-retainer professional-services packages;
- the explicit contract boundary: commercial support terms control legal SLA credits, named contacts, and any customer-specific restoration target.
The endpoint is read-only and guarded by access:read. It does not call an external
support system, write an outbox row, or mint a customer secret. Its job is to expose the
served product posture and standard Enterprise package catalog, while a signed license and
commercial agreement decide whether support is active for the deployment. The Platform page
renders the same data beside Editions and the managed-offering controls.
Federation (F41)
Cross-cluster / multi-region federation imports peer event logs into the local event log, then projects those imported events through the same read-model projector as local writes. That gives a passive region the same tenant registry, trust issuers, certificate inventory, audit facts, and other replayable read state without adding another datastore or copying PostgreSQL tables directly. Imported events keep their original event id, timestamp, tenant id, payload, actor, and schema version, so retries are duplicate-safe and the target region can rebuild from its own local log after failover.
The worker is off until an operator enables it. It runs under the existing leader election, so one control-plane replica imports a peer while every replica can serve the replicated read state. The peer cursor is durable per source cluster; a restart resumes from the last imported source sequence. A typical passive region points at the primary region's external NATS URL:
export TRSTCTL_FEDERATION_ENABLED=true
export TRSTCTL_FEDERATION_CLUSTER_ID=us-west-passive
export TRSTCTL_FEDERATION_REGION=us-west-2
export TRSTCTL_FEDERATION_PEER_ID=us-east-primary
export TRSTCTL_FEDERATION_PEER_REGION=us-east-1
export TRSTCTL_FEDERATION_PEER_NATS_URL=nats://nats.us-east.example:4222
export TRSTCTL_FEDERATION_INTERVAL=1s
export TRSTCTL_FEDERATION_RPO=5s
export TRSTCTL_FEDERATION_RTO=30s
The RPO target is the maximum import polling gap plus the health of the peer's replicated JetStream stream. The RTO target is the time for the passive region to finish local projection and accept traffic after you move clients or ingress. See Configuration and Run in production.
Use it
# the API spec (no auth needed) — point your tooling at it
curl -s https://trstctl.example.com/api/v1/openapi.json
# drive it from the CLI
trstctl-cli certificates list --limit 50
trstctl-cli audit events --type cert.issued --since 2026-01-01T00:00:00Z
trstctl-cli platform distribution
# one-binary evaluation: bundled datastores, supervised signer
TRSTCTL_POSTGRES_MODE=bundled TRSTCTL_NATS_MODE=embedded ./trstctl
The web console, browser /auth/login flow, SCIM /scim/v2 provisioning surface,
REST API, and CLI all drive the same served control plane. API tokens are still the
zero-dependency bootstrap path; OIDC, SCIM, and the ABAC deny overlay turn on only when
configured. See
Current limitations,
Install, and Configuration for production setup.
Pitfalls & limits
- Federation is passive-read-state replication — run one writable region for a tenant at a time, then promote a passive region during failover after its peer cursor and projection lag are inside your RPO/RTO target.
- TLS defaults to self-signed for instant start; set an operator cert
(
TRSTCTL_SERVER_TLS_MODE=file) for production. Plaintext mode requiresTRSTCTL_DEV_ALLOW_PLAINTEXT=trueand a loopback bind, so it is mechanically local-dev bounded. - Bundled datastores are for evaluation; run external PostgreSQL and NATS in production.
- The signer is a separate process by design — don't try to collapse it in; keeping private-key operations in their own isolated process is a security boundary.
Reference
- API: OpenAPI 3.1 at
GET /api/v1/openapi.json; RFC 7807 errors;Idempotency-Keyon mutations; cursor pagination;429+Retry-After; authenticated REST JSON request bodies are capped at 1 MiB and reject trailing JSON tokens. - CLI groups: one group per API route family (
owners,identities,certificates,secrets,discovery,nhi,breakglass,graph,risk,managed-keys,transit,ai,mcp, and the rest) — complete table in the CLI reference. - Auth:
/auth/login,/auth/callback,/auth/me,/auth/logout(OIDC whenauth.oidc.enabledis on);/auth/saml/login,/auth/saml/acs, and/auth/saml/metadata(SAML whenauth.saml.enabledis on);POST /auth/ldap/login(LDAP / Active Directory whenauth.ldap.enabledis on); API tokens prefixedtrst_. Config:TRSTCTL_AUTH_OIDC_ISSUER,TRSTCTL_AUTH_OIDC_CLIENT_ID,TRSTCTL_AUTH_OIDC_REDIRECT_URI,TRSTCTL_AUTH_OIDC_CLIENT_SECRET_TENANT,TRSTCTL_AUTH_OIDC_CLIENT_SECRET_REF,TRSTCTL_AUTH_SAML_ENTITY_ID,TRSTCTL_AUTH_SAML_ACS_URL,TRSTCTL_AUTH_SAML_IDP_METADATA_FILE,TRSTCTL_AUTH_LDAP_URL,TRSTCTL_AUTH_LDAP_GROUP_FILTER. - SCIM provisioning:
GET /scim/v2/ServiceProviderConfig,/scim/v2/Users, and/scim/v2/Groupswhenauth.scim.enabledis on. Config:TRSTCTL_AUTH_SCIM_ENABLED,TRSTCTL_AUTH_SCIM_TOKEN_TENANT_ID,TRSTCTL_AUTH_SCIM_TOKEN_FILE. - Authorization overlays: RBAC on every guarded route; ABAC deny overlay when
auth.abac.enabledis on. Config:TRSTCTL_AUTH_ABAC_ENABLED,TRSTCTL_AUTH_ABAC_MODULE,TRSTCTL_AUTH_ABAC_ENVIRONMENT. - Run modes:
TRSTCTL_POSTGRES_MODE(bundled/external),TRSTCTL_NATS_MODE(embedded/external),TRSTCTL_SERVER_TLS_MODE(internal/file/disabled). - Distribution posture:
GET /api/v1/platform/distributionandtrstctl-cli platform distributionexpose CAP-MODEL-01 self-hostable run-anywhere evidence receipts and CAP-MODEL-03 Air-gapped / on-prem + data residency receipts. - Federation (F41): event-log import with durable peer checkpoints, duplicate-safe event identity, and local read-model projection.
See also
The web console (every served surface in the browser) · Policy & governance (RBAC + audit) · Install · Configuration · Signing-service design · Current limitations · glossary: idempotency, mTLS, RLS, multi-tenancy
Covers: F10, F11, F12, F13, F14, F15, F40, F41