trstctl /docs Demo ↗ GitHub ↗

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-LARGE tier 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_support license mode (enabled, read_only, or off);
  • 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 requires TRSTCTL_DEV_ALLOW_PLAINTEXT=true and 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-Key on 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 when auth.oidc.enabled is on); /auth/saml/login, /auth/saml/acs, and /auth/saml/metadata (SAML when auth.saml.enabled is on); POST /auth/ldap/login (LDAP / Active Directory when auth.ldap.enabled is on); API tokens prefixed trst_. 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/Groups when auth.scim.enabled is 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.enabled is 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/distribution and trstctl-cli platform distribution expose 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

Rendered live from github.com/ctlplne/trstctl — found a mistake? edit this page.