Configuration
trstctl resolves its configuration from, in increasing precedence: built-in
defaults, an optional JSON config file (TRSTCTL_CONFIG_FILE), and environment
variables. The configuration is validated on boot — a bad combination fails
fast rather than starting half-configured.
Inspect the effective configuration at any time (credentials are redacted):
trstctl -check-config
Server
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_SERVER_ADDR |
:8443 |
Address the control plane listens on. |
TRSTCTL_SERVER_TLS_MODE |
internal |
internal (self-signed), file (operator cert), or disabled (plaintext, dev only). |
TRSTCTL_SERVER_TLS_CERT_FILE |
— | Server certificate chain (PEM); required when mode=file. |
TRSTCTL_SERVER_TLS_KEY_FILE |
— | Server private key (PEM); required when mode=file. |
TRSTCTL_DEV_ALLOW_PLAINTEXT |
false |
Explicit local-dev override required when TRSTCTL_SERVER_TLS_MODE=disabled; TRSTCTL_SERVER_ADDR must also bind loopback only. |
TRSTCTL_CORS_ALLOWED_ORIGINS |
empty (same-origin only) | Comma-separated exact browser Origins (scheme+host+port, e.g. https://console.example.com) allowed to make cross-origin, credentialed requests to the API (SEC-003). Empty means same-origin only: no Access-Control-Allow-Origin is emitted, so a cross-origin XHR is blocked by the browser. * is deliberately not honored for a credentialed API. |
TRSTCTL_LOG_LEVEL |
info |
debug, info, warn, or error. |
TRSTCTL_LOG_FORMAT |
json |
json or text. |
Transport encryption (TLS)
The control plane serves over TLS by default so no credential, token, or session ever travels in cleartext.
internal(default) — the control plane presents a self-signed certificate it generates at startup, coveringlocalhost,127.0.0.1, the container hostname, and the Compose service nametrstctl. Clients must trust it (or usecurl -k); suitable for evaluation and tightly controlled internal or air-gapped use. Public deployments must useserver.tls.mode=filewith an operator-provided certificate chain from your CA; do not expose the eval self-signed certificate to public clients.file— the control plane presents an operator-provided certificate and key. Use this in production with a certificate from your CA. A missing or malformed file fails fast at startup rather than falling back to plaintext.disabled— plaintext HTTP. Local development only and mechanically bounded: startup fails unlessTRSTCTL_DEV_ALLOW_PLAINTEXT=trueandTRSTCTL_SERVER_ADDRis loopback-only (localhost,127.0.0.1, or::1). Production TLS termination should useserver.tls.mode=fileat trstctl or a TLS-terminating proxy in front of a TLS-enabled trstctl listener; disabled mode is not the production proxy pattern.
The control-plane↔signer channel (the private keys live in a separate, isolated
process) is independent of this setting. The
default (single-binary child mode, and external mode with signer.socket)
is a peer-authenticated Unix domain socket — a 0600 socket in a 0700
directory, restricted to the signer's own uid via SO_PEERCRED on Linux — not a
TLS channel. For a separately-hosted signer across nodes, set
signer.mtls_address (with the signer.mtls_* certificate material): the control
plane then reaches the signer over mTLS — TLS 1.3, AEAD-only, with the control
plane and the signer each pinning the other's certificate (an untrusted or
merely CA-signed-but-unpinned peer is rejected, fail-closed). Exactly one of
signer.socket or signer.mtls_address is used in external mode; a partial mTLS
block fails closed at startup.
Datastores
trstctl stores its read state in PostgreSQL (the source-of-truth event log lives in NATS JetStream). PostgreSQL is the datastore in every deployment mode — there is no SQLite path.
!!! important "Datastores: bundled single-node for eval, external for production"
The serving binary (trstctl, via server.Run) can run a single-node eval
stack: bundled PostgreSQL (TRSTCTL_POSTGRES_MODE=bundled, the default — the
binary starts and supervises an embedded single-node Postgres with data under
TRSTCTL_POSTGRES_DATA_DIR on TRSTCTL_POSTGRES_PORT, default 5432) and
embedded NATS (TRSTCTL_NATS_MODE=embedded, the default — in-process
file-backed JetStream). Bundled PostgreSQL is available only for host archives
with committed runtime pins in deploy/supply-chain/embedded-postgres.json
(summarized in Supply chain):
currently linux-amd64, linux-arm64v8, and darwin-arm64v8. It downloads
that pinned PostgreSQL runtime once on first use, verifies the cached archive
before execution, and fails closed if the host archive is unsupported, unpinned,
or hash-mismatched. For production, use external for both:
TRSTCTL_POSTGRES_MODE=external with TRSTCTL_POSTGRES_DSN and
TRSTCTL_NATS_MODE=external with TRSTCTL_NATS_URL, which the Compose stack
and Helm chart wire up. There is no silently-failing default: an invalid
mode — or external without a DSN — fails fast at startup. (External mode
never downloads anything. --migrate / --backup target a managed datastore
and require external.)
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_POSTGRES_MODE |
bundled |
bundled (embedded single-node eval on a manifest-pinned host archive; downloads once and fails closed if unpinned) or external (managed cluster; recommended for production). |
TRSTCTL_POSTGRES_DSN |
— | Connection string; required when mode is external. |
TRSTCTL_POSTGRES_DATA_DIR |
data/postgres |
Data directory for the bundled datastore; eval data persists here across restarts. |
TRSTCTL_POSTGRES_PORT |
5432 |
Loopback port for the bundled datastore (override if 5432 is taken). |
TRSTCTL_POSTGRES_STATEMENT_TIMEOUT |
60s |
Server-side deadline applied to every statement (OPS-TIMEOUTS-001), so a stuck query fails closed instead of holding a connection indefinitely. DR rebuild/restore transactions widen this explicitly. |
TRSTCTL_POSTGRES_ACQUIRE_TIMEOUT |
10s |
How long a request may wait for a pooled PostgreSQL connection before failing closed with a structured 503. |
TRSTCTL_NATS_MODE |
embedded |
embedded (in-process file-backed JetStream for single-node eval) or external (NATS cluster; recommended for production). |
TRSTCTL_NATS_URL |
— | NATS URL; required when external (i.e. to serve). |
TRSTCTL_NATS_STORE_DIR |
data/nats |
JetStream store directory for the embedded datastore. |
TRSTCTL_NATS_REPLICAS |
3 in external, 1 embedded |
Required JetStream replicas for the source-of-truth event stream. External startup/readiness fail if NATS cannot honor the requested count. |
TRSTCTL_NATS_ALLOW_SINGLE_REPLICA |
false |
Eval-only opt-in that permits TRSTCTL_NATS_REPLICAS=1 in external mode. Do not enable it for production HA/RPO. |
TRSTCTL_NATS_SYNC_INTERVAL |
1s |
How often the embedded JetStream fsyncs the stream to stable storage (RESIL-001). nats-server's own default is ~2 minutes; trstctl tightens it so a single-node power loss loses at most ~1s of acked events. Only affects embedded mode; an external cluster manages its own durability. |
TRSTCTL_NATS_SYNC_ALWAYS |
false |
Fsyncs the embedded JetStream on every append (O_SYNC) instead of on the interval, for a near-zero single-node RPO at a throughput cost. Only affects embedded mode. |
External datastores
To point trstctl at managed PostgreSQL and NATS, switch both to external mode and supply their connection strings:
export TRSTCTL_POSTGRES_MODE=external
export TRSTCTL_POSTGRES_DSN='postgres://user:pass@db.internal:5432/trstctl?sslmode=require'
export TRSTCTL_NATS_MODE=external
export TRSTCTL_NATS_URL='nats://nats.internal:4222'
export TRSTCTL_NATS_REPLICAS=3
When a mode is external, its connection string is mandatory; trstctl refuses to
start without it. External NATS also refuses to serve under-replicated: the event
stream defaults to three replicas, startup fails on a non-clustered single NATS
server, and /readyz reports degraded if the observed stream later has fewer
replicas than configured. The Docker Compose eval stack uses the same external code
path but explicitly sets TRSTCTL_NATS_REPLICAS=1 and
TRSTCTL_NATS_ALLOW_SINGLE_REPLICA=true; keep that opt-in out of production.
Schema migrations
trstctl embeds its PostgreSQL schema as versioned SQL migrations and applies them itself; there is no separate migration binary.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_MIGRATE_AUTO |
true |
Applies pending schema migrations automatically on startup, serialized across replicas by the same PostgreSQL advisory lock leader election uses. Set false for a production posture where migrations are an explicit, backed-up step: a control plane that finds pending migrations then fails fast with guidance instead of changing the schema. |
trstctl --migrate applies pending migrations under the advisory lock and exits;
trstctl --migrate-status lists the pending plan (dry run) and exits. See
Database migrations & upgrades for the full upgrade/rollback runbook.
High availability (leader election and snapshots)
trstctl is safe to run as more than one control-plane replica against one shared external PostgreSQL and NATS (RESIL-002 / RESIL-004). With the defaults, a single replica behaves exactly as before; adding replicas needs no extra configuration beyond pointing them at the same datastores.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_HA_LEADER_ELECTION |
true (unset defaults on) |
Gates the continuous background workers — the projector tailer, outbox dispatcher, GC sweeps, CRL scheduler, audit-retention worker, and snapshot worker — behind a PostgreSQL session-scoped advisory lock so exactly one replica runs them; every replica still serves reads. Leave it on for any multi-replica deployment: turning it off with more than one replica reintroduces double-projection. The leader frees the lock automatically on crash, so a follower fails over with no lease tuning. |
TRSTCTL_HA_LEADER_CAMPAIGN_INTERVAL |
3s |
How often a follower retries to acquire leadership, and how often the leader re-checks it still holds the lock. Shorter gives faster failover at the cost of more try-lock probes. |
TRSTCTL_HA_SNAPSHOT_INTERVAL |
5m |
How often the leader persists a read-model snapshot at the current projection checkpoint (SPINE-007), so a later cold boot / DR restore replays only the tail instead of the full event log. Set 0 to disable periodic snapshots (boot then does a full checkpoint catch-up; the log stays the source of truth). |
Cross-cluster federation
Federation is disabled by default. When enabled on a passive cluster, the leader worker imports a peer's event log into the local event log, advances a durable peer cursor, and projects the imported events locally. The passive region therefore serves from its own PostgreSQL and NATS after failover; it does not read the primary region's PostgreSQL tables.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_FEDERATION_ENABLED |
false |
Enable the leader-only peer import worker. |
TRSTCTL_FEDERATION_CLUSTER_ID |
— | Stable id for this cluster, for example us-west-passive; required when enabled. |
TRSTCTL_FEDERATION_REGION |
— | Human/operator region label for this cluster. |
TRSTCTL_FEDERATION_PEER_ID |
— | Stable id of the source cluster; required for env-configured single-peer federation. |
TRSTCTL_FEDERATION_PEER_REGION |
— | Human/operator region label for the source cluster. |
TRSTCTL_FEDERATION_PEER_NATS_URL |
— | Source cluster NATS URL. The source must expose its trstctl event stream over external NATS. |
TRSTCTL_FEDERATION_INTERVAL |
1s |
How often the passive cluster polls the peer log. This is the main operator-tuned RPO knob. |
TRSTCTL_FEDERATION_RPO |
5s |
Operator target for maximum accepted replication lag. Use this in runbooks and monitoring. |
TRSTCTL_FEDERATION_RTO |
30s |
Operator target for passive-region promotion after traffic moves. |
JSON config can declare multiple peers under federation.peers; the environment
overlay above configures one common peer. Keep ha.leader_election on so one replica
owns imports while all replicas serve the replicated read state.
Lifecycle
How far ahead of expiry trstctl renews and alerts. Values are Go durations.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_LIFECYCLE_RENEW_BEFORE |
720h (30 days) |
Renew this far before expiry. |
TRSTCTL_LIFECYCLE_ALERT_BEFORE |
336h (14 days) |
Alert this far before expiry. |
Native connector and external-CA assembly
The 24 native deployment connectors and 14 external-CA drivers are compiled into
trstctl, but start deny-by-default: the operator chooses the exact integrations to
construct. The connector allowlist and its shared network policy — which native
connectors may be constructed, the shared HTTP timeout, private-CIDR grants, and the
loopback-only insecure-HTTP escape hatch — are flattened into the environment
variables below.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_CONNECTORS_ENABLED |
unset (none) | Comma-separated allowlist of native connector names to construct, drawn from the closed set compiled into the binary (nginx, apache, caddy, envoy, iis, haproxy, f5, netscaler, a10, kemp, cisco, fortigate, paloalto, postfix, traefik, aws-acm, azure-keyvault, gcp-certificate-manager, java-keystore, postgresql, mysql, rabbitmq, elasticsearch, tomcat). Names left out of the list construct no target. |
TRSTCTL_CONNECTORS_HTTP_TIMEOUT |
15s |
HTTP timeout applied to connector calls, for example the right_size PATCH/GET cycle. Must be a positive Go duration. |
TRSTCTL_CONNECTORS_ALLOW_PRIVATE_CIDRS |
unset | Comma-separated CIDRs explicitly granted to connector HTTP endpoints that resolve to a private address. |
TRSTCTL_CONNECTORS_ALLOW_INSECURE_HTTP |
false |
Loopback-only development escape hatch permitting an http:// connector endpoint (for example a local right_size emulator); it cannot authorize plaintext to a non-loopback host. |
The tenant-bound structured bindings — connectors.local_profiles (local
execution profiles), connectors.right_size (tenant-to-endpoint bindings), and every
external_cas entry — remain JSON/YAML config-file only, because they carry
tenant/provider associations where a parallel-list typo could cross a security
boundary.
A local connector needs both an enabled driver and an operator-owned execution profile. Tenant target JSON can then select that profile, but cannot invent a command or escape its allowed roots:
{
"connectors": {
"enabled": ["nginx", "f5"],
"http_timeout": "15s",
"allow_private_cidrs": ["10.40.0.0/16"],
"local_profiles": {
"nginx-prod": {
"allowed_roots": ["/etc/nginx/tls"],
"actions": [{
"logical_name": "nginx",
"command": "/usr/sbin/nginx",
"pass_args": true,
"timeout": "15s"
}]
}
},
"right_size": [{
"tenant_id": "11111111-1111-4111-8111-111111111111",
"connector": "least-privilege",
"endpoint": "https://entitlements.internal",
"token_ref": "secret://connectors/right-size-token"
}]
}
}
Local action commands must be absolute, executable, regular non-symlink files.
Unix shell interpreters are rejected. The one native-administration exception is
IIS PowerShell: its profile must pin the complete connector logical_args, set a
separate complete operator-owned args list, and leave pass_args false. The
runtime compares the connector request with that fixed logical argv and executes
only the fixed operator argv, so target data never becomes PowerShell source text.
connectors.right_size binds one tenant plus the playbook's connector name to an
operator endpoint and a same-tenant encrypted-secret reference. The durable
connector.right_size worker sends an idempotent PATCH, verifies the vendor's
mutation receipt, performs an authenticated GET readback, and only then records a
delivered connector receipt. Store the referenced token through the tenant secrets
surface (secrets.enable_api=true) before enabling the binding. An unconfigured
tenant/connector pair fails closed.
External-CA credentials use file:/absolute/path references. Files are loaded into
locked memory for one outbox attempt and wiped afterward. Network policy, private
CIDRs, custom roots, and mTLS identities are operator-owned:
{
"external_cas": [{
"id": "aws-pca-prod",
"type": "awspca",
"name": "Production AWS Private CA",
"endpoint": "https://acm-pca.us-east-1.amazonaws.com",
"region": "us-east-1",
"certificate_authority_arn": "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/UUID",
"access_key_id": "AKIA...",
"secret_access_key_ref": "file:/run/secrets/aws-pca-secret",
"network": {"timeout": "15s"}
}, {
"id": "azure-managed-hsm-ca",
"type": "azurekv",
"name": "Azure Managed HSM Issuing CA",
"tenant_id": "11111111-1111-4111-8111-111111111111",
"endpoint": "https://production.managedhsm.azure.net",
"managed_key_ref": "https://production.managedhsm.azure.net/keys/issuing-ca/0123456789abcdef",
"ca_cert_file": "/etc/trstctl/azure-issuing-ca-chain.pem",
"network": {"timeout": "15s"}
}, {
"id": "entrust-prod",
"type": "entrust",
"name": "Entrust CA Gateway",
"endpoint": "https://entrust-ca.internal",
"ca_id": "production-ca",
"network": {
"root_ca_file": "/etc/trstctl/entrust-server-ca.pem",
"client_cert_file": "/etc/trstctl/entrust-client.pem",
"client_key_file": "/run/secrets/entrust-client-key.pem",
"server_name": "entrust-ca.internal",
"timeout": "15s"
}
}, {
"id": "letsencrypt-prod",
"type": "letsencrypt",
"name": "Let's Encrypt",
"directory_url": "https://acme-v02.api.letsencrypt.org/directory"
}, {
"id": "shell-ca-prod",
"type": "shellca",
"name": "Isolated Shell CA",
"command": "/usr/local/libexec/trstctl-shell-ca",
"args": ["--profile", "production"],
"env_refs": {"CA_TOKEN": "file:/run/secrets/shell-ca-token"},
"network": {"timeout": "15s"}
}]
}
External-CA endpoints require HTTPS. The separate
network.allow_insecure_http escape hatch is accepted only for an HTTP
localhost, 127/8, or ::1 development emulator; the runtime client also
refuses any non-loopback resolution or cross-origin/scheme-changing redirect.
allow_private_endpoint and private CIDR grants never authorize plaintext.
The Azure CA block carries only routing and public trust material. tenant_id
binds the authority to one tenant, managed_key_ref is the opaque HTTPS key id
returned by that tenant's managed-key lifecycle, and ca_cert_file is the public
issuer chain whose leaf public key must match the managed key. Do not put an Azure
bearer token, key_name, or key_version in external_cas: Azure credentials and
private-key operations live in the isolated signer's managed_keys backend.
Let's Encrypt account JWS signatures also cross the signer transport. The control plane derives a stable account handle from the external-CA id and directory URL, but it receives only the public key and signatures; the ECDSA account private key never enters the HTTP process.
Entrust always requires the complete network mTLS identity shown above. The
client pins root_ca_file, verifies server_name, and presents the paired client
certificate/key. Missing, partial, or untrusted client material fails before an
Entrust enrollment request is sent; plaintext HTTP is not an Entrust mode.
For shellca, each env_refs key is a logical credential name, not a secret-valued
environment variable. The child sees CA_TOKEN_FD=<number> and reads the exact
credential bytes from that inherited anonymous pipe. It never receives CA_TOKEN,
and trstctl consumes and wipes the byte buffer after the one child attempt. No
credential pathname or filesystem-backed secret copy is created for the child.
Supported type values are adcs, awspca, azurekv, digicert, ejbca,
entrust, gcpcas, globalsign, letsencrypt, sectigo, shellca,
smallstep, vaultpki, and venafi. trstctl -check-config validates every
provider's required fields before the server accepts traffic. The exact connector
target schemas are listed in Deployment connectors.
Notifications
Notification channels are off until an operator configures them. When enabled, lifecycle
expiry alerts are written to the notification.* outbox first, then delivered by the
registered channel workers. Tenant-scoped routing policy authoring and channel-test
delivery are served from the console; channel secrets remain in operator-managed secret
references or files.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_NOTIFICATIONS_EMAIL_ENABLED |
false |
Enable SMTP email delivery. |
TRSTCTL_NOTIFICATIONS_EMAIL_SMTP_ADDR |
empty | SMTP relay host:port. |
TRSTCTL_NOTIFICATIONS_EMAIL_FROM |
empty | RFC 5322 From address. |
TRSTCTL_NOTIFICATIONS_EMAIL_TO |
empty | Comma-separated recipient addresses. |
TRSTCTL_NOTIFICATIONS_EMAIL_USERNAME |
empty | Optional SMTP username. |
TRSTCTL_NOTIFICATIONS_EMAIL_PASSWORD / TRSTCTL_NOTIFICATIONS_EMAIL_PASSWORD_FILE |
empty | Optional SMTP password as bytes or a file. |
TRSTCTL_NOTIFICATIONS_SLACK_ENABLED |
false |
Enable Slack incoming-webhook delivery. |
TRSTCTL_NOTIFICATIONS_SLACK_WEBHOOK_URL |
empty | Slack incoming-webhook URL. |
TRSTCTL_NOTIFICATIONS_TEAMS_ENABLED |
false |
Enable Microsoft Teams incoming-webhook delivery. |
TRSTCTL_NOTIFICATIONS_TEAMS_WEBHOOK_URL |
empty | Teams webhook URL. |
TRSTCTL_NOTIFICATIONS_SMS_ENABLED |
false |
Enable SMS gateway delivery. |
TRSTCTL_NOTIFICATIONS_SMS_ENDPOINT |
empty | HTTPS endpoint for the operator SMS gateway. |
TRSTCTL_NOTIFICATIONS_SMS_FROM |
empty | Optional sender label or number. |
TRSTCTL_NOTIFICATIONS_SMS_TO |
empty | Comma-separated SMS recipients. |
TRSTCTL_NOTIFICATIONS_SMS_TOKEN / TRSTCTL_NOTIFICATIONS_SMS_TOKEN_FILE |
empty | Optional gateway bearer token as bytes or a file. |
TRSTCTL_NOTIFICATIONS_SIEM_ENABLED |
false |
Enable SIEM collector delivery. |
TRSTCTL_NOTIFICATIONS_SIEM_ENDPOINT |
empty | HTTPS endpoint for the SIEM collector or forwarding gateway. |
TRSTCTL_NOTIFICATIONS_SIEM_TOKEN / TRSTCTL_NOTIFICATIONS_SIEM_TOKEN_FILE |
empty | Optional collector bearer token as bytes or a file. |
TRSTCTL_NOTIFICATIONS_SIEM_SOURCE |
trstctl |
Source label in SIEM events. |
TRSTCTL_NOTIFICATIONS_PAGERDUTY_ENABLED |
false |
Enable native PagerDuty Events API v2 delivery. |
TRSTCTL_NOTIFICATIONS_PAGERDUTY_ENDPOINT |
PagerDuty public Events v2 endpoint | Override the enqueue URL; production endpoints must use HTTPS. |
TRSTCTL_NOTIFICATIONS_PAGERDUTY_ROUTING_KEY / TRSTCTL_NOTIFICATIONS_PAGERDUTY_ROUTING_KEY_FILE |
empty | PagerDuty integration routing key. Set exactly one when enabled; the file form keeps the key out of the environment. |
TRSTCTL_NOTIFICATIONS_PAGERDUTY_TIMEOUT |
10s |
Bounded Events API request deadline. |
TRSTCTL_NOTIFICATIONS_PAGERDUTY_ALLOW_PRIVATE_CIDRS |
empty | Comma-separated exact private CIDRs allowed by the SSRF-safe client for an operator-run gateway. |
TRSTCTL_NOTIFICATIONS_OPSGENIE_ENABLED |
false |
Enable native OpsGenie Alert API v2 delivery. |
TRSTCTL_NOTIFICATIONS_OPSGENIE_ENDPOINT |
OpsGenie public Alert API endpoint | Override the create-alert URL; production endpoints must use HTTPS. |
TRSTCTL_NOTIFICATIONS_OPSGENIE_API_KEY / TRSTCTL_NOTIFICATIONS_OPSGENIE_API_KEY_FILE |
empty | OpsGenie API key. Set exactly one when enabled; the file form keeps the key out of the environment. |
TRSTCTL_NOTIFICATIONS_OPSGENIE_TIMEOUT |
10s |
Bounded Alert API request deadline. |
TRSTCTL_NOTIFICATIONS_OPSGENIE_ALLOW_PRIVATE_CIDRS |
empty | Comma-separated exact private CIDRs allowed by the SSRF-safe client for an operator-run gateway. |
PagerDuty and OpsGenie credentials are copied into locked, non-dumpable memory at
startup and wiped on shutdown. Both integrations send deterministic vendor idempotency
identifiers, require the vendor's exact acceptance receipt, and redact remote error
bodies. *_ALLOW_INSECURE_HTTP exists only for a loopback development emulator; even
when set, it cannot enable cleartext delivery to a non-loopback host.
Code signing
The shipped code-signing routes stay fail-closed until code_signing.enabled is true.
Tenant-to-key and tenant-to-OIDC associations are structured JSON only: putting these
parallel lists in environment variables would make it too easy to attach one tenant's
trust to another tenant. A minimal key-backed plus GitHub Actions keyless configuration
looks like this:
{
"code_signing": {
"enabled": true,
"keys": [{
"tenant_id": "tenant-acme",
"id": "release-key",
"handle": "acme-release-code-signing",
"algorithm": "ecdsa-p256",
"create_if_missing": true
}],
"github_oidc_tenants": [{
"tenant_id": "tenant-acme",
"issuer": "https://token.actions.githubusercontent.com",
"audience": "sigstore",
"jwks_file": "/run/trstctl/github-actions-jwks.json",
"allowed_owners": ["acme"]
}],
"ephemeral_algorithm": "ecdsa-p256",
"rekor": {
"endpoint": "https://rekor.example.com/api/v1/log/entries",
"timeout": "10s",
"log_public_key_file": "/run/trstctl/rekor-log-public-key.pem"
}
}
}
create_if_missing provisions a code-sign-purpose-constrained handle inside the
separate signer process; the control plane never receives the private key. Keyless
requests also create and destroy their one-use key inside that signer. Pin the Rekor
log public key from the Rekor operator's authenticated distribution channel. Startup
fails if this trust anchor is absent or malformed, and every outbox delivery verifies
the returned signed-entry timestamp against it before acknowledging the row.
Code signing uses the shared live policy controls rather than a decorative, code-only
allow list. With ca.policy.enabled=true, OPA receives action code_sign, the
authenticated actor, the configured key ID (or keyless:<verified-san>), and
attrs.digest_sha256; evaluation errors and policy-pool saturation fail closed. With
ca.policy.require_approval=true, a denied response returns
approval_required:codesign:<sha256>. A distinct certs:issue approver posts
{"action":"sign"} to /api/v1/identities/{that-resource}/approvals, after which the
requester retries the same signing tuple with a new Idempotency-Key. The resource hash
binds tenant, authenticated requester, key/keyless identity, and exact digest.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_CODE_SIGNING_ENABLED |
false |
Enable production assembly of the served key-backed and keyless routes. |
TRSTCTL_CODE_SIGNING_EPHEMERAL_ALGORITHM |
ecdsa-p256 |
Algorithm for isolated one-use keyless keys; currently only P-256 is accepted. |
TRSTCTL_CODE_SIGNING_REKOR_ENDPOINT |
Sigstore public Rekor v1 endpoint | HashedRekord create endpoint. Production endpoints must use HTTPS. |
TRSTCTL_CODE_SIGNING_REKOR_TIMEOUT |
10s |
Bounded create/readback deadline. |
TRSTCTL_CODE_SIGNING_REKOR_LOG_PUBLIC_KEY_FILE |
empty | Required PEM PKIX public key used to verify Rekor signed-entry timestamps. |
TRSTCTL_CODE_SIGNING_REKOR_ALLOW_PRIVATE_CIDRS |
empty | Comma-separated exact private CIDRs allowed for an operator-run Rekor deployment. |
TRSTCTL_CODE_SIGNING_REKOR_ALLOW_INSECURE_HTTP is accepted only for a loopback
development emulator. It cannot relax transport security for a remote Rekor service.
Telemetry
Telemetry is off by default and never sends anything unless you opt in. When enabled, it sends only coarse, anonymized, non-PII data.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_TELEMETRY_ENABLED |
false |
Set true to opt in. A malformed value is ignored (stays off). |
TRSTCTL_TELEMETRY_ENDPOINT |
https://telemetry.trstctl.com/v1/usage |
Where reports go; must be https. |
TRSTCTL_TELEMETRY_INTERVAL |
24h |
Reporting interval. |
TRSTCTL_TELEMETRY_INSTANCE_ID_FILE |
data/telemetry/instance-id |
Local file holding the random anonymous instance ID. Required when telemetry is enabled. |
See Telemetry for exactly what is and is not collected.
Air-gap and outbound egress
Air-gap mode arms the no-phone-home outbound HTTP(S) guard. It is off by default;
when enabled, public destinations are blocked unless you explicitly allow a host
or CIDR. It is intended for disconnected networks and is paired with the Helm
values-airgap.yaml overlay described in Air-gapped install.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AIRGAP_ENABLED |
false |
Set true to deny public outbound HTTP(S) unless a host or CIDR is allowlisted. |
TRSTCTL_AIRGAP_ALLOW_PRIVATE |
true |
Allows loopback, private, and link-local IP destinations. Leave true for private PostgreSQL/NATS/OTLP/local model endpoints. |
TRSTCTL_AIRGAP_ALLOW_HOSTS |
— | Comma-separated host allowlist for operator-owned local services such as otel-collector.observability.svc. Hosts only; URLs are rejected. |
TRSTCTL_AIRGAP_ALLOW_CIDRS |
— | Comma-separated CIDR allowlist for private service ranges, e.g. 10.0.0.0/8,172.16.0.0/12. Invalid CIDRs fail startup. |
TRSTCTL_OUTBOUND_ENV_CREDENTIAL_REFS |
— | Comma-separated env:NAME references that API-authored discovery and response-integration requests may use for outbound credentials. Unknown env refs are rejected before outbox enqueue. |
When TRSTCTL_AIRGAP_ENABLED=true, trstctl rejects
TRSTCTL_TELEMETRY_ENABLED=true and TRSTCTL_AI_MODEL_MODE=cloud at startup.
Local OTLP collectors and local AI runtimes are still allowed when their hosts or
CIDRs are private or explicitly allowlisted.
ITSM and ServiceNow bindings
ServiceNow ticket creation is fail-closed until an operator pre-registers the
destination URL and credential reference. Requests to
/api/v1/itsm/servicenow/tickets and ServiceNow response-dispatch destinations must
match that binding exactly; callers cannot send the ServiceNow token to an arbitrary
instance_url or opt into a private endpoint unless the binding permits it.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_SERVICENOW_INSTANCE_URL |
— | Approved ServiceNow instance base URL, for example https://example.service-now.com. |
TRSTCTL_SERVICENOW_TOKEN_REF |
env:TRSTCTL_SERVICENOW_TOKEN when an instance URL is set |
Credential reference resolved by the outbox worker. This is a reference, not the token value. |
TRSTCTL_SERVICENOW_ALLOW_PRIVATE_ENDPOINT |
false |
Allows an approved http:// or private endpoint binding for lab/private ServiceNow gateways. Leave false for normal hosted ServiceNow. |
TRSTCTL_SERVICENOW_PRIVATE_EGRESS_CIDRS |
— | Comma-separated CIDR grants for a private ServiceNow binding. Required when TRSTCTL_SERVICENOW_ALLOW_PRIVATE_ENDPOINT=true. |
JSON config supports the same policy as itsm.servicenow.bindings[] with
instance_url, token_ref, allow_private_endpoint, and
private_egress_cidrs. Private endpoint requests also require the caller to hold
the dedicated egress:private permission.
OpenTelemetry export
OTLP export is off by default and sends data only to the collector endpoint you configure. It is separate from product telemetry: use it to feed your own OpenTelemetry Collector, Splunk, Datadog, or SIEM pipeline with served HTTP traces and audit-event log records.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_OTLP_ENABLED |
false |
Set true to enable OTLP/HTTP protobuf export. |
TRSTCTL_OTLP_ENDPOINT |
— | Absolute collector URL. HTTPS is required unless TRSTCTL_OTLP_INSECURE=true; the exporter posts to /v1/traces and /v1/logs under this base. |
TRSTCTL_OTLP_INSECURE |
false |
Allows plaintext http:// collector endpoints for local or private-network deployments. |
TRSTCTL_OTLP_BEARER_TOKEN |
— | Optional collector bearer token. Prefer the file setting for production secret mounts. |
TRSTCTL_OTLP_BEARER_TOKEN_FILE |
— | Optional file containing the collector bearer token. Mutually exclusive with TRSTCTL_OTLP_BEARER_TOKEN. |
TRSTCTL_OTLP_TIMEOUT |
5s |
Per-export HTTP timeout. |
TRSTCTL_OTLP_QUEUE_SIZE |
1024 |
Bounded trace-export queue size. Full queues drop spans rather than slowing served API requests. |
TRSTCTL_OTLP_SERVICE_NAME |
trstctl |
service.name resource attribute sent to the collector. |
In air-gap mode, point OTLP at an operator-owned collector on a private address or
add the collector host to TRSTCTL_AIRGAP_ALLOW_HOSTS; public collector SaaS
endpoints are blocked by the egress guard.
Audit
The audit trail is a projection of the event log; these settings govern its evidence export and retention policy. See Audit trail & compliance for the trust model and what trstctl enables vs. what you must operate.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AUDIT_SIGNING_KEY_FILE |
data/audit/signing-key.pem |
PEM path for the evidence-export signing key. It is persisted (created 0600 on first boot) so signed bundles verify across restarts; the key no longer rotates each restart. |
TRSTCTL_AUDIT_RETENTION |
— (indefinite) | Served audit-view window, a Go duration (e.g. 8760h). Empty means indefinite (the default). When set and TRSTCTL_AUDIT_ARCHIVE_DIR is given, a background worker archives older records to signed bundles, verifies the bundle, and advances a replayable tenant checkpoint so those records leave the live query view. Their underlying AN-2 event envelopes remain retained for projection rebuild, disaster recovery, and authorized privacy rewrite. |
TRSTCTL_AUDIT_ARCHIVE_DIR |
— | Cold-storage directory for the signed archive bundles (<dir>/<tenant>/audit-<seq>.jws, 0600). Required to advance the served-view retention floor; without it the view remains indefinite. Point it at WORM-backed storage you protect. See Audit retention and archive lifecycle. |
The audit query (/api/v1/audit/events) and signed export (/api/v1/audit/export)
endpoints are wired into the serving binary, so they return real data — not an
error — out of the box. Protect the signing key file and back it up; distribute
its public half to auditors out of band.
Privacy Retention
Non-audit personal data is retained by class, then pseudonymized after the
operational need ends. This is separate from audit retention: the immutable event
trail remains the source of truth, while tenant read surfaces stop carrying stale
names, emails, subjects, SANs, comments, profile authors, approval actors, and
free-form evidence. The worker emits privacy.retention.enforced and projects the
anonymization from that event, so rebuilds replay the same result.
Subject erasure also pseudonymizes matching raw subject bytes in the hot JetStream
event store. The control plane records privacy.subject.erased, builds a
sequence-preserving replacement generation with the erased subject changed to its
tenant-bound erased:<subject_ref> placeholder, and signs the old/new continuity
evidence before switching authority. The old generation remains authoritative if
the operation stops before that switch; only after the signed replacement is active
are its superseded subject bytes securely scrubbed. Replay, projection tailing,
retention, and backups share a history barrier with that cutover. This keeps future
hot-log replay, audit queries, and full backups taken after the erasure from carrying
the raw subject without ever purging and rebuilding the sole authoritative stream.
Backups and signed audit archives created before the erasure are not rewritten
automatically. Record the outcome of your legal-hold, WORM,
backup-deletion, or cryptographic-shredding procedure with
POST /api/v1/privacy/archive-erasure-attestations; inspect the tenant evidence
ledger with GET /api/v1/privacy/archive-erasure-attestations or
trstctl privacy archives attest/list. The attestation event stores
subject_ref and redacted evidence refs, not the raw subject.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_PRIVACY_RETENTION_ENABLED |
true |
Runs the leader-only non-audit PII retention worker. Set false only when an external privacy job enforces the same policy. |
TRSTCTL_PRIVACY_RETENTION_INTERVAL |
24h |
Worker cadence. It also runs once on startup. |
TRSTCTL_PRIVACY_RETENTION_OWNERS |
17520h (730 days) |
Pseudonymize owner name/email when the owner is older than this and no identity references it. |
TRSTCTL_PRIVACY_RETENTION_IDENTITIES |
9528h (397 days) |
Pseudonymize terminal or expired identity metadata. |
TRSTCTL_PRIVACY_RETENTION_CERTIFICATES |
9528h (397 days) |
Pseudonymize expired/revoked/superseded certificate subject, SAN, deployment location, and source metadata. |
TRSTCTL_PRIVACY_RETENTION_SSH_KEYS |
4320h (180 days) |
Clear orphaned stale SSH key comments and locations. |
TRSTCTL_PRIVACY_RETENTION_ACCESS |
2160h (90 days) |
Pseudonymize offboarded tenant members and expired/revoked API-token subjects. |
TRSTCTL_PRIVACY_RETENTION_APPROVALS |
9528h (397 days) |
Pseudonymize old requester/approver subject values while preserving resource/action evidence. |
TRSTCTL_PRIVACY_RETENTION_PROFILES |
9528h (397 days) |
Pseudonymize old certificate-profile author values. |
TRSTCTL_PRIVACY_RETENTION_ATTESTATIONS |
9528h (397 days) |
Clear stale free-form attestation evidence JSON. |
TRSTCTL_PRIVACY_RETENTION_AGENTS |
4320h (180 days) |
Pseudonymize stale agent names while preserving agent id, status, version, and heartbeat timestamps. |
Operators can trigger and inspect the same served path with
POST /api/v1/privacy/retention-runs, GET /api/v1/privacy/retention-runs, or
the matching trstctl privacy retention run/list CLI commands.
Browser SSO
Browser sign-on is optional. Scoped API tokens still work when browser sign-on is off. When it is on, each verified OIDC, SAML, or LDAP / Active Directory user must map to exactly one trstctl tenant by subject, tenant claim, directory group, or an explicit single-tenant fallback. Missing mappings fail the login closed instead of silently dropping a user into the wrong tenant.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AUTH_OIDC_ENABLED |
false |
Enables served OIDC login at /auth/login and /auth/callback. |
TRSTCTL_AUTH_OIDC_ISSUER |
unset | Expected OIDC issuer. |
TRSTCTL_AUTH_OIDC_AUTHORIZATION_RESPONSE_ISS_PARAMETER_SUPPORTED |
false |
Requires the callback iss parameter to match the issuer when the IdP advertises RFC 9207 support. |
TRSTCTL_AUTH_OIDC_CLIENT_ID |
unset | Expected OIDC audience / client id. |
TRSTCTL_AUTH_OIDC_CLIENT_SECRET |
unset | Inline confidential-client secret for the code→token exchange. Required for a confidential client unless the tenant-scoped credential-store reference below is used instead; a public/PKCE client may leave it empty. |
TRSTCTL_AUTH_OIDC_CLIENT_SECRET_TENANT / TRSTCTL_AUTH_OIDC_CLIENT_SECRET_REF |
unset | Reads a confidential-client secret from the encrypted tenant-scoped credential store at (tenant, auth.oidc, ref, client_secret). |
TRSTCTL_AUTH_OIDC_AUTH_ENDPOINT |
unset | The IdP's authorization endpoint the browser is redirected to. Required. |
TRSTCTL_AUTH_OIDC_TOKEN_ENDPOINT |
unset | The IdP's token endpoint the callback exchanges the authorization code at. Required. |
TRSTCTL_AUTH_OIDC_REDIRECT_URI |
unset | External callback URL, usually https://trstctl.example.com/auth/callback. |
TRSTCTL_AUTH_OIDC_JWKS_FILE / TRSTCTL_AUTH_OIDC_JWKS_JSON |
unset | IdP signing keys used for offline id_token verification. |
TRSTCTL_AUTH_SAML_ENABLED |
false |
Enables the served SAML 2.0 SP. |
TRSTCTL_AUTH_SAML_ENTITY_ID |
unset | Stable SP entity ID, often the metadata URL. |
TRSTCTL_AUTH_SAML_METADATA_URL |
unset | External URL for /auth/saml/metadata. |
TRSTCTL_AUTH_SAML_ACS_URL |
unset | External assertion consumer service URL for /auth/saml/acs. |
TRSTCTL_AUTH_SAML_IDP_METADATA_FILE / TRSTCTL_AUTH_SAML_IDP_METADATA_XML |
unset | IdP metadata XML containing the signing certificate. |
TRSTCTL_AUTH_SAML_SESSION_SECRET_FILE |
unset | HMAC secret file used to sign browser sessions. |
TRSTCTL_AUTH_SAML_TENANT_CLAIM |
unset | SAML attribute whose value feeds tenant mapping. |
TRSTCTL_AUTH_SAML_GROUPS_CLAIM |
unset | SAML attribute whose values feed group-to-tenant mapping. |
TRSTCTL_AUTH_LDAP_ENABLED |
false |
Enables served LDAP / Active Directory login at POST /auth/ldap/login. |
TRSTCTL_AUTH_LDAP_URL |
unset | Directory URL. Use ldaps:// for production; ldap:// is accepted only on loopback. |
TRSTCTL_AUTH_LDAP_USER_DN_TEMPLATE |
unset | Direct-bind DN template such as uid={username},ou=people,dc=example,dc=org. |
TRSTCTL_AUTH_LDAP_BIND_DN / TRSTCTL_AUTH_LDAP_BIND_PASSWORD_FILE |
unset | Optional read-only service bind for user and group searches. |
TRSTCTL_AUTH_LDAP_USER_SEARCH_BASE_DN / TRSTCTL_AUTH_LDAP_USER_FILTER |
unset | User lookup when no direct-bind template is used. |
TRSTCTL_AUTH_LDAP_GROUP_SEARCH_BASE_DN / TRSTCTL_AUTH_LDAP_GROUP_FILTER |
unset | Group lookup; {user_dn} and {username} are escaped before search. |
TRSTCTL_AUTH_LDAP_GROUP_NAME_ATTRIBUTE |
unset | Group attribute mapped to tenant_mappings[].group, usually cn. |
TRSTCTL_AUTH_LDAP_SESSION_SECRET_FILE |
unset | HMAC secret file used to sign browser sessions. |
SCIM provisioning
SCIM 2.0 provisioning is optional and separate from browser sign-on. Enable it when
your identity provider should push users and groups into trstctl instead of an
operator maintaining role membership by hand. The served endpoint is /scim/v2:
IdPs use POST /scim/v2/Users, PATCH /scim/v2/Users/{id},
POST /scim/v2/Groups, and PATCH /scim/v2/Groups/{id}.
Each SCIM bearer token is bound to exactly one tenant in config. trstctl reads the raw
token from auth.scim.tokens[].token_file, hashes it at startup, wipes the raw bytes,
and keeps only the hash. The token selects the tenant for every SCIM request; tenant
ids in SCIM payloads are ignored. Groups map to configured RBAC role names, so an IdP
group with display name viewer grants the built-in viewer role to its members.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AUTH_SCIM_ENABLED |
false |
Enables the served SCIM 2.0 provisioning surface under /scim/v2. |
TRSTCTL_AUTH_SCIM_TOKEN_NAME |
scim |
Human label used in audit actor metadata for the single env-configured token. |
TRSTCTL_AUTH_SCIM_TOKEN_TENANT_ID |
unset | Tenant this SCIM token may provision. Required when SCIM is enabled through env. |
TRSTCTL_AUTH_SCIM_TOKEN_FILE |
unset | File containing the raw bearer token. Required when SCIM is enabled through env. |
Example multi-token SCIM config:
auth:
scim:
enabled: true
tokens:
- name: okta-payments
tenant_id: 22222222-2222-2222-2222-222222222222
token_file: /etc/trstctl/scim/okta-payments.token
- name: entra-platform
tenant_id: 33333333-3333-3333-3333-333333333333
token_file: /etc/trstctl/scim/entra-platform.token
Example SAML config:
auth:
saml:
enabled: true
entity_id: https://trstctl.example.com/auth/saml/metadata
metadata_url: https://trstctl.example.com/auth/saml/metadata
acs_url: https://trstctl.example.com/auth/saml/acs
idp_metadata_file: /etc/trstctl/idp-metadata.xml
session_secret_file: /var/lib/trstctl/saml-session.secret
tenant_claim: tenant
groups_claim: groups
tenant_mappings:
- subject: alice@example.com
tenant_id: 11111111-1111-1111-1111-111111111111
roles: [admin]
Example LDAP / Active Directory config:
auth:
ldap:
enabled: true
url: ldaps://ad.example.com:636
bind_dn: cn=trstctl-reader,ou=service-accounts,dc=example,dc=com
bind_password_file: /etc/trstctl/ldap-bind.secret
user_search_base_dn: ou=people,dc=example,dc=com
user_filter: "(sAMAccountName={username})"
group_search_base_dn: ou=groups,dc=example,dc=com
group_filter: "(member={user_dn})"
group_name_attribute: cn
email_attribute: mail
session_secret_file: /var/lib/trstctl/ldap-session.secret
tenant_mappings:
- group: payments-trstctl-admins
tenant_id: 22222222-2222-2222-2222-222222222222
roles: [admin]
ABAC deny overlay
ABAC is an optional deny-only overlay on top of RBAC. RBAC must grant the permission first; the ABAC Rego module can then block the request using request attributes, identity tags, operator-provided environment state, and time. This is useful for rules such as "prod certs may issue only during change windows" or "break-glass actions may run only from the platform project."
The module must declare package trstctl.abac, define boolean deny, and may define
string reason. Bad Rego fails startup. Runtime evaluation errors deny the request, and
policy-worker saturation returns 503 instead of allowing.
In a config file, the keys are auth.abac.enabled, auth.abac.module, and
auth.abac.environment.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AUTH_ABAC_ENABLED |
false |
Enables the ABAC deny overlay after RBAC on guarded API routes and on served issue/deploy/revoke lifecycle decisions. |
TRSTCTL_AUTH_ABAC_MODULE |
unset | Inline Rego module with package trstctl.abac, boolean deny, and optional string reason. Required when ABAC is enabled. |
TRSTCTL_AUTH_ABAC_ENVIRONMENT |
unset | Comma-separated operator state copied into input.env, for example change_window=true,region=us-east-1. |
Example ABAC config:
auth:
abac:
enabled: true
environment:
change_window: "false"
module: |
package trstctl.abac
default deny := false
default reason := ""
deny if {
input.permission == "certs:issue"
input.resource.env == "prod"
input.env.change_window != "true"
}
reason := "prod certificates may issue only during a change window" if {
deny
}
Break-glass lifecycle and reconciliation
Break-glass emergency issuance has two served paths. POST /api/v1/breakglass/issue
performs online m-of-n emergency issuance when a signer-backed break-glass issuer is
configured. First open the exact CSR/reason/TTL ceremony at
POST /api/v1/breakglass/issue-ceremonies; then distinct configured operators approve
that ceremony through the shared authenticated CA-approval route. The execution
request carries ceremony_id and no approver names. The server derives quorum only
from immutable ca.ceremony.approved event actors, consumes the ceremony once,
returns a self-verifying bundle, and records breakglass.issued before responding.
The same signer-backed lifecycle exposes purpose-bound rotation and target-CA
cross-sign ceremony/execution pairs under /api/v1/breakglass/*-ceremonies,
/api/v1/breakglass/rotate, and /api/v1/breakglass/cross-sign.
POST /api/v1/breakglass/reconcile handles recovery after an offline ceremony: it
accepts signed offline bundles, verifies them against trust anchors pinned in process
config, and records verified facts as breakglass.issued events in the hash-chained
audit log. Requests cannot supply their own verifier keys.
breakglass.enabled pins reconciliation verifier material. The files may be DER, or
PEM with CERTIFICATE and PUBLIC KEY blocks, and startup fails closed when either
is missing or unreadable. breakglass.online_enabled additionally requires one
tenant, an already-persisted dual-control signer handle whose public key matches both
files, a distinct operator roster, and a threshold of at least two. Reconciliation-
only deployments do not need the online fields.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_BREAKGLASS_ENABLED |
false |
Enables verifier material for POST /api/v1/breakglass/reconcile. |
TRSTCTL_BREAKGLASS_ONLINE_ENABLED |
false |
Enables signer-backed issue, rotation, and cross-sign routes. Requires enabled=true and all custody/quorum fields below. |
TRSTCTL_BREAKGLASS_CA_CERT_FILE |
unset | DER or PEM CA certificate that emergency bundle certificates must chain to. Required when enabled. |
TRSTCTL_BREAKGLASS_PUBLIC_KEY_FILE |
unset | DER or PEM public key that verifies the emergency bundle manifest signature. Required when enabled. |
TRSTCTL_BREAKGLASS_TENANT_ID |
unset | The only tenant allowed to use the online break-glass signer. |
TRSTCTL_BREAKGLASS_SIGNER_HANDLE |
unset | Existing purpose-constrained, dual-control signer handle whose public key must match the configured CA and public-key file. |
TRSTCTL_BREAKGLASS_OPERATORS |
unset | Comma-separated authenticated operator subjects allowed to count toward quorum. |
TRSTCTL_BREAKGLASS_THRESHOLD |
0 |
Required m-of-n threshold; online mode requires at least 2 and no more than the number of distinct configured operators. |
Example break-glass reconciliation config:
breakglass:
enabled: true
ca_cert_file: /etc/trstctl/breakglass-ca.pem
public_key_file: /etc/trstctl/breakglass-public-key.pem
Online lifecycle adds the custody and roster fields:
breakglass:
enabled: true
online_enabled: true
ca_cert_file: /etc/trstctl/breakglass-ca.pem
public_key_file: /etc/trstctl/breakglass-public-key.pem
tenant_id: 10000000-0000-4000-8000-000000000001
signer_handle: recovery-breakglass-ca
operators: [recovery-operator-a, recovery-operator-b, recovery-operator-c]
threshold: 2
To create that handle without a test-only or library-only path, leave
online_enabled=false, use trstctl-cli ca ceremonies start plus distinct
ca ceremonies approve calls, and finish with ca authorities create-root. Save
the returned certificate_pem and signer_handle; derive the public-key file from
the certificate (openssl x509 -pubkey -noout), set the online fields above, and
restart. Startup proves the certificate, derived public key, and persisted signer
handle all name the same key before it exposes any online break-glass route.
Secrets (credentials at rest)
Upstream CA and connector credentials — API keys, passwords, client secrets — are stored encrypted at rest using envelope encryption (R3.1): a fresh random data-encryption key (DEK) encrypts each credential with AES-256-GCM, and the key-encryption key (KEK) wraps the DEK. Only ciphertext is ever persisted; the plaintext never appears in the database, in config dumps, or in logs. The cryptography lives behind the platform's single crypto boundary.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_SECRETS_KEK_FILE |
data/secrets/kek.bin |
Path to the 256-bit KEK that wraps every stored credential. It is created 0600 on first boot if absent, and is the root of trust for credentials at rest. |
TRSTCTL_TENANT_SEAL_LOCAL_WRAPPER_ID |
unset | Stable non-secret ID for one operator-provisioned local tenant-domain wrapper. Both this variable and the file variable below are required together. The database stores this ID, never the path or wrapper key. |
TRSTCTL_TENANT_SEAL_LOCAL_WRAPPER_FILE |
unset | Existing local wrapper-key file for the ID above. trstctl does not create it or fall back to the deployment KEK when it is missing/wrong. Configure multiple wrappers with secrets.tenant_seal_local_wrappers in JSON/YAML. |
TRSTCTL_IDEMPOTENCY_RESULT_FLEET_READY |
false |
Operator assertion that every process writing this PostgreSQL database understands sealed-row-v1 and durable indeterminate claims. After the legacy drain, startup installs a sealed-only PostgreSQL default/constraint. Never enable it while an older writer is running; the ratchet is deliberately incompatible and is not inferred from one node seeing zero rows. |
TRSTCTL_SECRETS_ENABLE_API |
false |
Enables the served /api/v1/secrets/* surface, including store, dynamic leases, sharing, PKI secret issuance, machine login, sync, and Gitleaks scans. It also enables the Vault/OpenBao-compatible common aliases under /v1/auth/token/lookup-self, /v1/secret/data/*, and /v1/pki/issue/*. |
TRSTCTL_SECRETS_AUTH_SECRET_FILE |
unset | Optional HMAC key file for machine-login token credentials. When unset, the login method fails closed while other secrets routes continue to work. |
TRSTCTL_SECRETS_GITLEAKS_BIN |
auto-detect | Path to the pinned Gitleaks v8.27.2 binary used by POST /api/v1/secrets/scans. Empty resolves TRSTCTL_GITLEAKS_BIN, tools/bin/gitleaks, then PATH. Run tools/gitleaks/install.sh during image build or host provisioning to install the supported checksum-verified release tarball. A missing binary makes scan requests fail closed with 503. |
Every default-binary idempotency result is outer-sealed through the same
tenant-aware crypto resolver before PostgreSQL retains it. A legacy tenant uses
the deployment KEK explicitly. An opted-in tenant names exactly one separately
provisioned wrapper and never trial-decrypts or falls back to the deployment KEK.
The local wrapper file is opened only inside a tenant-scoped shared database
fence; its domain key lives in locked memory for that callback and is then wiped.
Before the HTTP mutation surface becomes ready, startup drains historical
raw-v0 and sealed-dynamic-lease-v1 cache rows in key-ordered, RLS-scoped
batches into the authenticated sealed-row-v1 envelope. Each replacement is a
compare-and-swap over tenant, key, binding, codec, and prior bytes. A crash
therefore resumes safely, while a changed row or unavailable tenant wrapper
fails readiness without logging or embedding the result bytes in the error.
The runtime reader rejects both legacy codecs after this drain. When the
fleet-ready assertion is set, startup also locks the table, proves every
completed row is a CSL sealed container, changes the database default, and
validates a permanent sealed-only constraint before readiness. This is a
one-way compatibility decision: drain or stop all old writers first.
{
"secrets": {
"kek_file": "/run/secrets/deployment-kek",
"tenant_seal_local_wrappers": [
{"id": "operator-a", "file": "/run/custody/operator-a-wrapper"},
{"id": "operator-b", "file": "/run/custody/operator-b-wrapper"}
]
}
}
Wrapper files are operator authority, not ordinary application data. Provision them separately with restrictive ownership/mode before configuring a tenant. Missing, corrupt, or mismatched custody fails that tenant closed while other tenants continue. This local mode makes no outbound call and works air-gapped.
Dynamic-secret providers and outbound sync targets use structured JSON/YAML because
each entry is bound to exactly one tenant. Authority-bearing values are references,
not inline strings: file:/absolute/path loads an operator-owned 0600 file, while
secret://path opens that tenant's encrypted secret-store row for one outbox attempt.
Both are copied into locked, non-dumpable memory and destroyed after the provider call.
{
"secrets": {"enable_api": true},
"secret_integrations": {
"dynamic_providers": [{
"tenant_id": "11111111-1111-4111-8111-111111111111",
"id": "orders-postgres",
"type": "postgresql",
"admin_dsn_ref": "file:/run/secrets/orders-postgres-admin-dsn",
"database": "orders",
"schema": "public",
"allowed_roles": ["reader"],
"max_ttl": "1h",
"username_prefix": "trstctl_orders"
}],
"sync_targets": [{
"tenant_id": "11111111-1111-4111-8111-111111111111",
"id": "orders-github",
"type": "github-actions",
"endpoint": "https://api.github.com",
"owner": "example",
"repo": "orders",
"token_ref": "secret://integrations/github-actions-token"
}]
}
}
Dynamic type values are postgresql, mysql, mongodb, aws-iam, gcp-iam,
azure-entra, kubernetes, and redis. Sync type values are
aws-secrets-manager, gcp-secret-manager, azure-key-vault, github-actions,
gitlab-ci, vercel, generic-ci-json, kubernetes-secrets,
terraform-cloud-opentofu, and vault-kv-v2. Startup validates each provider's
required native fields, role bindings, endpoint scheme, private-egress CIDRs, and
credential-reference form before accepting traffic. An absent tenant target or
provider fails its served mutation closed; it never falls back to another tenant or
to a test registry.
AWS, GCP, and Azure sync targets can explicitly replace their static target
credential with tenant-owned workload identity. Set exactly the provider switch
(aws_workload_identity, gcp_workload_identity, or
azure_workload_identity) on the matching target. The switch is false by default
and forbids the corresponding static credential fields when true. The workload
proof itself stays behind a file: or tenant-scoped secret:// reference.
Only the bounded secret-sync outbox worker opens and validates that proof, POSTs the
provider exchange, caches the locked short-lived token until its refresh boundary,
and feeds it into the existing hand-written target client. The request handler never
performs that egress. Air-gapped mode records offline_disabled, fails that queued
delivery once, and makes no token or target request.
Azure uses an Entra federated credential that accepts the already validated OIDC
proof directly as the OAuth client_assertion. This is simpler than a
certificate-signed assertion: it adds no private key or certificate custody. Create
the matching federated-credential binding on the Entra application, then configure
the Key Vault target:
secret_integrations:
sync_targets:
- tenant_id: 11111111-1111-4111-8111-111111111111
id: payments-azure-key-vault
type: azure-key-vault
endpoint: https://payments.vault.azure.net
azure_workload_identity: true
# Optional. When omitted, the source's azure_tenant_id selects:
# https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
# workload_identity_endpoint: https://login.microsoftonline.com/.../oauth2/v2.0/token
Create the tenant-scoped source through the console or
trstctl-cli secrets syncs workload-identities create --body-file source.json.
For Azure, the request sets provider: "azure", azure_tenant_id, the Entra
application client_id, and an allowed Key Vault target_scope such as
https://vault.azure.net/.default. It also binds the exact OIDC audience and
subject, target_id, allowed remote-key prefixes, workload_proof_ref, and a
JWT/JWKS trust_source_id. Those routing fields are stored under PostgreSQL RLS;
proof bytes and minted bearer tokens are never stored in the source row, event, job,
or API response.
Every dynamic-provider and sync-target HTTP endpoint must use HTTPS in production.
allow_private_endpoint grants a named private destination; it never grants plaintext.
Local emulators may set allow_insecure_loopback: true, but only beside an http://
endpoint whose host is exactly localhost, an address in 127.0.0.0/8, or ::1.
The runtime client resolves and dials loopback only, so changing DNS or adding a private
CIDR cannot stretch this development switch to a LAN, VPC, metadata service, or public
host.
Machine-auth methods beyond the HMAC token are configured in the JSON/YAML config
file under secrets.machine_auth. Each entry names one method: kubernetes,
aws-iam, gcp, azure, oidc, or jwt. JWT-family methods require
audience plus jwks_file or jwks_json, and must set either tenant_claim
(credential-bound tenancy) or tenant_id (tenant-pinned config). AWS IAM must set
tenant_id and allowed_accounts or allowed_arns because STS does not carry a
trstctl tenant claim.
Treat the credential-store KEK like the audit signing key: protect it and back it
up (a lost KEK means sealed credentials cannot be opened) with the same care
described in the disaster-recovery runbook. This
credential-store KEK is still a local key file. Do not confuse it with Helm
externalKMS, which applies to the signer's CA key-store DEK wrapping described in
Signer topology and CA custody.
On reload, local KEK, auth-secret, and session-secret files are accepted only if
they are regular files, not symlinks, owned by the process user with
0600-or-stricter permissions or mounted as root-owned Kubernetes Secret files
readable by the pod's fsGroup, and all parent directories reject group/world
writes. Unsafe restored files fail startup instead of silently weakening key
custody.
Backup
Event-log backups (trstctl --backup) are always integrity-protected (a SHA-256
trailer, plus an HMAC derived from TRSTCTL_AUDIT_SIGNING_KEY_FILE when one is
configured). They require the live deployment's external PostgreSQL DSN as well
as external NATS: PostgreSQL holds a shared history-generation barrier for the
complete export, preventing a concurrent privacy rewrite from changing which
JetStream generation is authoritative mid-backup. A full backup
(trstctl --full-backup-dir) additionally captures operational secrets — the audit
signing key, the signer authorization secret, and the sealed signer key store — so
production full backups require an operator-held encryption key.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_BACKUP_ENCRYPTION_KEY_FILE |
unset | Raw key-material file (normally 32 random bytes, never itself copied into the backup) used to AES-256-GCM-encrypt every sensitive full-backup artifact. Required for --full-backup-dir unless the override below is set. |
TRSTCTL_BACKUP_ALLOW_UNENCRYPTED |
false |
Break-glass override that permits an unencrypted full backup for a lab/export case. The choice is recorded in the backup manifest so an auditor can see the locked box was not used. |
See the disaster-recovery runbook for the full backup/restore procedure, including what each artifact covers and how to restore a signer host.
License
trstctl ships as a single open-core binary. Enterprise-tier features (managed keys, PCAS, HA support, FIPS artifact posture, remediation, PQC, governance, agent delegation, reconciliation, and verifiable decommission — the full Enterprise row in Editions) unlock through an offline, no-phone-home license check: the file is verified locally against public keys baked into the binary at release time. No configured file means Community edition; a corrupt or untrusted file fails startup loudly; an expired file still loads and walks a grace ladder so licensed read paths stay observable.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_LICENSE_FILE |
unset (Community edition) | Path to the signed license file (Ed25519-verified offline, the AN-9 attach seam). |
The related TRSTCTL_PCAS_* variable surface (about two dozen settings covering
delegation, recovery, federation, KEM, checkpoints, monitors, and retirement for
patent-covered credential algorithm succession) is off by default and gated by this
same license check; it is documented together with the rest of the PCAS material in
PCAS operations rather than duplicated here.
Conditional managed-key adapters (six served providers)
The managed-key lifecycle is off by default and requires an Enterprise license plus startup configuration. AWS KMS, Azure Key Vault / Managed HSM, GCP Cloud KMS, PKCS#11, TPM 2.0, and YubiHSM 2 are all required/census-served through the shipped cgo HSM signer artifact. “Conditional” means the route is inert until the operator selects and provisions one provider; it no longer means library-only. The control plane records a tenant event and PostgreSQL outbox command, while the isolated signer constructs the provider and performs the private operation. The control-plane process never receives provider credentials.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_MANAGED_KEYS_ENABLED |
false |
Enables licensed managed-key event/outbox assembly. When false, the routes fail closed; no provider is constructed. |
TRSTCTL_MANAGED_KEYS_PROVIDER |
aws |
Custody provider: aws, azure-key-vault, gcp-kms, pkcs11, tpm2, or yubihsm2. Selection is startup-static ordinary interface injection, not a runtime plugin engine. |
TRSTCTL_MANAGED_KEYS_AWS_REGION |
unset | AWS region for KMS, for example us-east-1. Required when enabled. |
TRSTCTL_MANAGED_KEYS_AWS_ENDPOINT |
unset | Optional absolute HTTPS endpoint override, used for VPC endpoints or partitions. Leave unset for regional AWS KMS. |
TRSTCTL_MANAGED_KEYS_AWS_ALLOW_INSECURE_LOOPBACK |
false |
Development-only plaintext opt-in. It is accepted only when the AWS endpoint is http://localhost, http://127.0.0.0/8, or http://[::1]; resolved non-loopback addresses still fail at dial time. |
TRSTCTL_MANAGED_KEYS_AWS_ACCESS_KEY_ID |
unset | AWS access key id. Required for the current served AWS KMS backend. |
TRSTCTL_MANAGED_KEYS_AWS_SECRET_ACCESS_KEY |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the file variant. |
TRSTCTL_MANAGED_KEYS_AWS_SECRET_ACCESS_KEY_FILE |
unset | File containing the AWS secret access key. Startup reads it, constructs the backend, and wipes the temporary file buffer. |
TRSTCTL_MANAGED_KEYS_AWS_SESSION_TOKEN |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the optional file variant. |
TRSTCTL_MANAGED_KEYS_AWS_SESSION_TOKEN_FILE |
unset | Optional file containing the temporary session token. |
TRSTCTL_MANAGED_KEYS_AWS_PRIVATE_EGRESS_CIDRS |
unset | Comma-separated private CIDRs explicitly granted to an AWS private/VPC endpoint. Metadata and link-local destinations remain blocked. |
TRSTCTL_MANAGED_KEYS_AZURE_VAULT_URL |
unset | Azure Key Vault or Managed HSM vault URL, for example https://trstctl-prod.managedhsm.azure.net. Required when provider is azure-key-vault. |
TRSTCTL_MANAGED_KEYS_AZURE_ENDPOINT |
unset | Optional absolute HTTPS endpoint override for private endpoints. Leave unset for the vault URL. |
TRSTCTL_MANAGED_KEYS_AZURE_ALLOW_INSECURE_LOOPBACK |
false |
Development-only plaintext opt-in restricted to a loopback vault_url or endpoint; it cannot authorize private-network or public HTTP. |
TRSTCTL_MANAGED_KEYS_AZURE_BEARER_TOKEN |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the file variant. |
TRSTCTL_MANAGED_KEYS_AZURE_BEARER_TOKEN_FILE |
unset | File containing the Azure bearer token. Startup reads it, constructs the backend, and wipes the temporary file buffer. |
TRSTCTL_MANAGED_KEYS_AZURE_PRIVATE_EGRESS_CIDRS |
unset | Comma-separated private CIDRs explicitly granted to an Azure private endpoint. Metadata and link-local destinations remain blocked. |
TRSTCTL_MANAGED_KEYS_GCP_PARENT |
unset | GCP Cloud KMS key-ring resource, for example projects/P/locations/L/keyRings/R. Required when provider is gcp-kms. |
TRSTCTL_MANAGED_KEYS_GCP_ENDPOINT |
unset | Optional absolute HTTPS endpoint override for private service endpoints. Leave unset for https://cloudkms.googleapis.com/v1. |
TRSTCTL_MANAGED_KEYS_GCP_ALLOW_INSECURE_LOOPBACK |
false |
Development-only plaintext opt-in restricted to an HTTP loopback endpoint; it cannot authorize private-network or public HTTP. |
TRSTCTL_MANAGED_KEYS_GCP_BEARER_TOKEN |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the file variant. |
TRSTCTL_MANAGED_KEYS_GCP_BEARER_TOKEN_FILE |
unset | File containing the GCP bearer token. Startup reads it, constructs the backend, and wipes the temporary file buffer. |
TRSTCTL_MANAGED_KEYS_GCP_PRIVATE_EGRESS_CIDRS |
unset | Comma-separated private CIDRs explicitly granted to a GCP private service endpoint. Metadata and link-local destinations remain blocked. |
TRSTCTL_MANAGED_KEYS_PKCS11_MODULE_PATH |
unset | Native PKCS#11 module path, for example libsofthsm2.so, nShield cknfast, or Luna Cryptoki. Required when provider is pkcs11. |
TRSTCTL_MANAGED_KEYS_PKCS11_TOKEN_LABEL |
unset | Initialized token label to log in to. Required when provider is pkcs11. |
TRSTCTL_MANAGED_KEYS_PKCS11_USER_PIN |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the file variant. |
TRSTCTL_MANAGED_KEYS_PKCS11_USER_PIN_FILE |
unset | File containing the PKCS#11 user PIN. Startup reads it, constructs the backend, and wipes the temporary file buffer. |
TRSTCTL_MANAGED_KEYS_PKCS11_KEY_LABEL_PREFIX |
trstctl-pkcs11 |
Label prefix for generated token objects. |
TRSTCTL_MANAGED_KEYS_TPM2_PATH |
unset | Linux TPM device (for example /dev/tpmrm0) or swtpm Unix socket. Required for tpm2. |
TRSTCTL_MANAGED_KEYS_TPM2_OWNER_AUTH |
unset | Inline compatibility input. The isolated child-signer path rejects it; use OWNER_AUTH_FILE when hierarchy auth is required. |
TRSTCTL_MANAGED_KEYS_TPM2_OWNER_AUTH_FILE |
unset | Optional file containing TPM owner-hierarchy authorization. |
TRSTCTL_MANAGED_KEYS_TPM2_KEY_AUTH |
unset | Inline compatibility input. The isolated child-signer path rejects it; use KEY_AUTH_FILE. |
TRSTCTL_MANAGED_KEYS_TPM2_KEY_AUTH_FILE |
unset | Optional file containing authorization assigned to generated signing objects. |
TRSTCTL_MANAGED_KEYS_TPM2_PERSISTENT_HANDLE_BASE |
0x81010000 |
First signer-owned persistent handle. The first 256 handles remain the legacy fresh-key bank; operation-aware keys deterministically probe the remaining persistent range and carry the full durable-operation digest in immutable TPM Public.AuthPolicy. Foreign occupied handles are skipped, never adopted or overwritten. |
TRSTCTL_MANAGED_KEYS_YUBIHSM2_MODULE_PATH |
unset | Path to Yubico's yubihsm_pkcs11 module. Required for yubihsm2. |
TRSTCTL_MANAGED_KEYS_YUBIHSM2_TOKEN_LABEL |
unset | Token/connector label selected through the vendor PKCS#11 ABI. |
TRSTCTL_MANAGED_KEYS_YUBIHSM2_USER_PIN |
unset | Inline compatibility input. The isolated child-signer path rejects it; use the file variant. |
TRSTCTL_MANAGED_KEYS_YUBIHSM2_USER_PIN_FILE |
unset | File containing the YubiHSM authentication value/PIN. |
TRSTCTL_MANAGED_KEYS_YUBIHSM2_KEY_LABEL_PREFIX |
trstctl-pkcs11 |
Label prefix for generated YubiHSM signing objects. |
| Provider | Shipped binding | Required DoD substrate |
|---|---|---|
aws |
AWS SDK v2 asymmetric KMS | Faithful SigV4 KMS emulator |
azure-key-vault |
Azure Keys/Managed HSM data plane | Faithful Managed HSM wire emulator |
gcp-kms |
GCP Cloud KMS REST data plane | Faithful resource/digest emulator |
pkcs11 |
Native cgo PKCS#11 module | SoftHSM plus independent pkcs11-tool readback |
tpm2 |
google/go-tpm persistent object driver |
swtpm plus independent tpm2-tools readback |
yubihsm2 |
Yubico PKCS#11 connector ABI | SoftHSM-backed vendor-ABI emulator |
Development-only LocalStack shape (not conformance or shipped-runtime proof):
export TRSTCTL_MANAGED_KEYS_ENABLED=true
export TRSTCTL_MANAGED_KEYS_PROVIDER=aws
export TRSTCTL_MANAGED_KEYS_AWS_REGION=us-east-1
export TRSTCTL_MANAGED_KEYS_AWS_ENDPOINT=http://127.0.0.1:4566
export TRSTCTL_MANAGED_KEYS_AWS_ALLOW_INSECURE_LOOPBACK=true
export TRSTCTL_MANAGED_KEYS_AWS_ACCESS_KEY_ID=test
printf '%s' test > /tmp/localstack-kms-secret
chmod 0600 /tmp/localstack-kms-secret
export TRSTCTL_MANAGED_KEYS_AWS_SECRET_ACCESS_KEY_FILE=/tmp/localstack-kms-secret
Private endpoint CIDR grants control where HTTPS may go. They never weaken TLS.
The three ALLOW_INSECURE_LOOPBACK switches are deliberately separate and accept
only loopback HTTP for a same-host emulator.
Example production shape:
managed_keys:
enabled: true
provider: aws
aws:
region: us-east-1
access_key_id: AKIA...
secret_access_key_file: /etc/trstctl/aws-kms-secret-access-key
Example Azure Key Vault HSM shape:
managed_keys:
enabled: true
provider: azure-key-vault
azure:
vault_url: https://trstctl-prod.managedhsm.azure.net
bearer_token_file: /etc/trstctl/azure-kv-token
Example GCP Cloud KMS shape:
managed_keys:
enabled: true
provider: gcp-kms
gcp:
parent: projects/prod/locations/us/keyRings/trstctl
bearer_token_file: /etc/trstctl/gcp-kms-token
Example PKCS#11 HSM shape:
managed_keys:
enabled: true
provider: pkcs11
pkcs11:
module_path: /usr/lib/softhsm/libsofthsm2.so
token_label: trstctl-prod
user_pin_file: /etc/trstctl/pkcs11-user-pin
key_label_prefix: trstctl-ca
Example TPM 2.0 shape:
managed_keys:
enabled: true
provider: tpm2
tpm2:
path: /dev/tpmrm0
owner_auth_file: /etc/trstctl/tpm-owner-auth
key_auth_file: /etc/trstctl/tpm-key-auth
persistent_handle_base: 2164326400 # 0x81010000
Example YubiHSM 2 shape:
managed_keys:
enabled: true
provider: yubihsm2
yubihsm2:
module_path: /usr/lib/yubihsm_pkcs11.so
token_label: trstctl-prod
user_pin_file: /etc/trstctl/yubihsm-auth
key_label_prefix: trstctl-ca
Static no-cgo signer builds fail closed if pkcs11 or yubihsm2 is selected.
Use the published artifact built by deploy/docker/Dockerfile.signer-hsm; its
release profile enables cgo and installs /usr/local/bin/trstctl-signer-hsm.
When the licensed attach seam succeeds, operators with keys:write can exercise
POST /api/v1/managed-keys and the rotate/revoke/zeroize API and CLI shapes. Before
each destructive action, two different principals with keys:approve record the
exact opaque key_id and rotate, revoke, or zeroize action through
POST /api/v1/managed-keys/approvals; the requester cannot self-approve. Requests
require Idempotency-Key; lifecycle events omit private bytes; tenant projections
use PostgreSQL RLS; and provider work is delivered by the durable outbox to the
separate signer. The signer writes an fsync-backed operation intent before provider I/O.
Every shipped provider also carries that identity into provider state: an atomic AWS tag,
deterministic Azure/GCP resource identity, deterministic PKCS#11 CKA_ID (including
YubiHSM), or a full-width TPM public tag plus deterministic handle probing. If a provider
commits and the signer dies before journaling the response, restart finds the same effect;
revoke/zeroize likewise confirm terminal provider state rather than blindly repeating a
mutation. Managed-key outbox circuits are partitioned by provider and current key (or
operation ID before a key exists), so one unavailable key does not stall every managed-key
command. All six exact backend census rows are SERVED + REQUIRED.
For an externally deployed signer, mount the same file-backed provider descriptor
and pass it with --managed-keys-config plus the signed --license; never copy a
credential into argv or an inline JSON field.
Signer topology and CA custody
The private-key operations run in a separate, sacred process, so the CA keys never live in the API process. Its issuing CA key is persisted, sealed at rest (R3.2) so a restart preserves the CA instead of silently rotating it. The signer can run two ways:
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_SIGNER_MODE |
child |
child: the control plane supervises trstctl-signer as a child process (single binary). external: it connects to a separately deployed signer service over a UDS (TRSTCTL_SIGNER_SOCKET) or, across nodes, mTLS (TRSTCTL_SIGNER_MTLS_ADDRESS). |
TRSTCTL_SIGNER_SOCKET |
— | The signer's Unix-domain socket. In external mode set either this or TRSTCTL_SIGNER_MTLS_ADDRESS; in child mode a temp socket is used if unset. |
TRSTCTL_SIGNER_CALL_TIMEOUT |
10s |
Bounds every signer RPC that lacks a tighter caller deadline (OPS-TIMEOUTS-001), so a hung signer fails closed instead of stalling issuance. Accepts 1s..2m. |
TRSTCTL_SIGNER_KEY_STORE_DIR |
data/signer/keys |
Directory where the signer seals its keys at rest (child mode passes it to the signer; in external mode set it on the signer service). |
TRSTCTL_SIGNER_AUTH_SECRET_FILE |
data/signer/sign-auth.bin |
Signer-side content-authorization verifier secret. The signer uses it to verify dual-control tokens before using privileged handles. Do not mount it into the control plane in production. |
TRSTCTL_SIGNER_AUTH_TOKEN_COMMAND |
— | Independent approval-token command used by the control plane in production. The command receives sign-intent JSON on stdin and returns the raw token as base64 on stdout. |
TRSTCTL_SIGNER_ALLOW_CO_RESIDENT_AUTHORIZER |
true in single-node eval defaults |
Evaluation-only escape hatch that lets the control plane mint signer tokens from TRSTCTL_SIGNER_AUTH_SECRET_FILE. Production-like external NATS deployments reject it; use TRSTCTL_SIGNER_AUTH_TOKEN_COMMAND instead. |
TRSTCTL_SIGNER_ALLOW_INSECURE_DEV_NONLINUX |
false |
Local-development-only escape hatch for running child signer mode on non-Linux hosts. Without it, trstctl-signer refuses startup when process hardening, UDS peer UID checks, and locked memory are unavailable. Do not set it in production. |
TRSTCTL_SIGNER_MTLS_ADDRESS |
— | host:port of a separately-hosted signer's mTLS listener. When set (in external mode), the control plane reaches the signer over TLS 1.3 mutual auth with both-ways certificate pinning instead of a UDS. Mutually exclusive with TRSTCTL_SIGNER_SOCKET. |
TRSTCTL_SIGNER_MTLS_SERVER_NAME |
— | The signer certificate's expected SAN, verified by the control plane. Required when TRSTCTL_SIGNER_MTLS_ADDRESS is set. |
TRSTCTL_SIGNER_MTLS_CERT_FILE / TRSTCTL_SIGNER_MTLS_KEY_FILE |
— | The control plane's own client certificate and key (PEM) presented on the mTLS channel. Required with …_MTLS_ADDRESS. |
TRSTCTL_SIGNER_MTLS_PEER_CA_FILE |
— | PEM CA bundle anchoring the signer's certificate. Required with …_MTLS_ADDRESS. |
TRSTCTL_SIGNER_MTLS_PEER_PIN |
— | Hex SHA-256 of the signer certificate's public key, pinned by the control plane. Required with …_MTLS_ADDRESS. |
TRSTCTL_CA_CERT_FILE |
data/ca/issuing-ca.crt |
Where the issuing CA's self-signed certificate is persisted, so the control plane reuses the same CA cert across restarts. |
In Helm deployments, the signer key store uses local KEK custody by default:
kek.existingSecret or eval-only kek.generate=true mounts
/etc/trstctl/kek/kek.bin and the chart passes --kek to trstctl-signer.
Regulated deployments can instead set:
externalKMS:
enabled: true
provider: awskms # awskms | gcpkms | azurekv | pkcs11
keyRef: arn:aws:kms:us-east-1:111122223333:key/trstctl-signer
wrapCommand: /usr/local/bin/trstctl-kms-wrap
timeout: 10s
With externalKMS.enabled=true, the chart passes --kms-provider,
--kms-key-ref, --kms-wrap-command, and --kms-timeout to the signer and does
not mount the local KEK Secret. The signer invokes the adapter without a shell as
<wrapCommand> wrap|unwrap <provider> <keyRef>, with DEK bytes only on
stdin/stdout. Missing provider/keyRef/command values, unsupported provider names,
or a relative command path fail at template time.
Back up the sealed key store, the signer custody input, and the CA cert together
(the CA-key recovery set) per the disaster-recovery runbook.
For local-KEK mode that custody input is the signer KEK Secret; for externalKMS
mode it is access to the same provider keyRef plus the wrapper adapter and provider
credentials. The
docker-compose.yml
runs the signer as its own service in external mode.
Regulated CA governance mode
The individual issuance controls — the OPA/Rego policy gate, four-eyes dual
control, a bound default certificate profile, revocation publication, and FIPS — can
each be enabled on their own. For a compliance deployment that is error-prone: a
single missing control silently weakens the posture. ca.governance_mode=regulated
is the one coherent switch that closes that gap. In regulated mode the binary
fails startup unless all of these are present together, each with an
actionable error naming the field to set:
- the OPA policy gate is on (
ca.policy.enabled=true); - four-eyes dual control is on (
ca.policy.require_approval=true) with at least two distinct approvers (ca.policy.required_approvalsunset, defaulting to 2, or>= 2) — a single approver is rejected; - a default certificate profile is bound (
ca.default_profile); - revocation publication is configured — at least one of
ca.crl_distribution_pointsorca.ocsp_servers— so issued leaves carry a status pointer (composing with the served-leaf profile); - and, when
ca.require_fips=trueis declared, the FIPS 140-3 module is active (the binary was built withGOFIPS140=v1.0.0/make fips-build, or run withGODEBUG=fips140=on).
A complete regulated config boots normally. The default posture
(ca.governance_mode unset, or standard) imposes no coupling, so existing
single-node deployments are unaffected.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_CA_GOVERNANCE_MODE |
standard |
standard (or unset): the controls are independent. regulated: fail startup unless the policy gate, four-eyes dual control, a bound default profile, revocation publication, and any declared FIPS requirement are all present together. |
TRSTCTL_CA_REQUIRE_FIPS |
false |
In regulated mode, additionally require the FIPS 140-3 module to be active (build with GOFIPS140=v1.0.0 or run with GODEBUG=fips140=on); otherwise startup fails closed. Ignored outside regulated mode. |
Served AI surface and model adapter
The AI/RCA/MCP surface is off by default. MCP investigation tools are read-only when
enabled; MCP write tools require the separate TRSTCTL_AI_MCP_WRITE_TOOLS=true
operator opt-in. The model adapter is separately off by default: with
TRSTCTL_AI_MODEL_MODE=off (or unset), query/RCA still return grounded citations, but
no prompt leaves the process.
GET /api/v1/ai/status reports the live enabled state, model mode, endpoint host,
egress class, and redaction/refusal posture without echoing the full endpoint URL.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AI_ENABLE_API |
false |
Serve /api/v1/ai/status, /api/v1/ai/query, /api/v1/ai/rca, and /api/v1/mcp/tools* behind auth/RBAC. |
TRSTCTL_AI_MCP_IDENTITY |
— | Workload identity label the MCP server presents. |
TRSTCTL_AI_MCP_WRITE_TOOLS |
false |
Expose guarded MCP write tools (issue_certificate, rotate_certificate). Calls still require certs:issue, an Idempotency-Key, and are audited as mcp.tool.write. |
TRSTCTL_AI_RATE_MAX |
60 |
Per-caller MCP tool-call budget per window. |
TRSTCTL_AI_RATE_WINDOW_SECONDS |
60 |
MCP tool-call rate window in seconds. |
TRSTCTL_AI_MODEL_MODE |
off |
off, local, or cloud. off means no model adapter and no prompt egress. |
TRSTCTL_AI_MODEL_RUNTIME |
— | Local runtime label, required with mode=local: ollama or vllm. |
TRSTCTL_AI_MODEL_PROVIDER |
— | Cloud/gateway provider label, required with mode=cloud. |
TRSTCTL_AI_MODEL_ENDPOINT |
— | Completion endpoint. Local http:// endpoints must be loopback; otherwise use HTTPS. Cloud endpoints must be HTTPS. URL userinfo is rejected so credentials are not stored in config. |
TRSTCTL_AI_MODEL_NAME |
— | Model name sent to the configured endpoint. Required with mode=local or mode=cloud. |
TRSTCTL_AI_MODEL_ALLOW_EGRESS |
false |
Required as true with mode=cloud; invalid for off and local. |
Ollama local mode sends the native generate shape to the endpoint. vLLM and cloud mode send an OpenAI-compatible chat-completions shape. Every model path goes through the boundary redactor and residual-secret refusal gate before the HTTP request is made; if no model is configured, the answer remains citation-grounded and air-gapped.
Served protocol listeners
ACME, EST, SCEP, CMP, SPIFFE, SSH, and KMIP protocol surfaces are opt-in until they are explicitly bound to a tenant. That startup check is intentional: a public protocol endpoint must know the tenant it acts for before it is exposed. KMIP is a raw mTLS TCP listener, not an HTTP route, so it additionally requires server certificate/key files and a client CA trust anchor.
For the blank evaluation stack, TRSTCTL_PROTOCOLS_PROFILE=eval is a bounded shortcut:
it assembles ACME, EST, SCEP, CMP, SSH, TSA, and SPIFFE for exactly
TRSTCTL_PROTOCOLS_EVAL_TENANT_ID. Those responders stay behind a closed runtime gate
until a tenant-authenticated operator activates them in the first-run wizard or with
POST /api/v1/setup/protocols/activate. Activation appends an immutable tenant event
before the HTTP routes and SPIFFE socket become reachable, and replay restores the gate
after restart. The shortcut never enables KMIP. Production can omit PROFILE and use
the individual toggles below.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_PROTOCOLS_PROFILE |
— | Set to eval only for the guided evaluation profile. Empty keeps the exact individual, default-off production toggles. |
TRSTCTL_PROTOCOLS_EVAL_TENANT_ID |
— | Required with PROFILE=eval; binds every eval responder and its activation event to one tenant. |
TRSTCTL_PROTOCOLS_EVAL_SPIFFE_TRUST_DOMAIN |
eval.trstctl.local |
SPIFFE trust domain used by the eval profile. The explicit SPIFFE setting below remains the production control. |
TRSTCTL_PROTOCOLS_ACME_ENABLED / …_TENANT_ID |
false / — |
Serve ACME at /directory + /acme/... for the named tenant. |
TRSTCTL_PROTOCOLS_ACME_EAB_REQUIRED |
false |
Require RFC 8555 External Account Binding on ACME newAccount; /directory advertises externalAccountRequired. |
TRSTCTL_PROTOCOLS_ACME_EAB_KEY_ID |
— | Public EAB kid accepted by the ACME server. The single env shortcut configures one key; JSON config can carry multiple protocols.acme_eab.keys. |
TRSTCTL_PROTOCOLS_ACME_EAB_HMAC_KEY / …_FILE |
— | Byte-backed HS256 HMAC key for the EAB kid; use the file form for production secret injection. |
TRSTCTL_PROTOCOLS_ACME_MAX_NONCES |
4096 |
Maximum outstanding ACME replay nonces retained by the tenant-bound ACME mount. |
TRSTCTL_PROTOCOLS_ACME_MAX_ACCOUNTS |
2048 |
Maximum ACME accounts retained by the tenant-bound ACME mount. |
TRSTCTL_PROTOCOLS_ACME_MAX_PENDING_ORDERS |
4096 |
Maximum pending ACME orders retained before the server returns ACME rateLimited (429). |
TRSTCTL_PROTOCOLS_ACME_MAX_PENDING_AUTHORIZATIONS |
8192 |
Maximum pending ACME authorizations retained across pending orders. |
TRSTCTL_PROTOCOLS_ACME_MAX_PENDING_CHALLENGES |
24576 |
Maximum pending ACME challenge records retained across pending authorizations. |
TRSTCTL_PROTOCOLS_ACME_MAX_PENDING_ORDERS_PER_ACCOUNT |
128 |
Per-account pending-order cap, independent from source-IP budgets. |
TRSTCTL_PROTOCOLS_ACME_MAX_NEW_NONCES_PER_SOURCE |
120 |
Per-source newNonce budget per source window. |
TRSTCTL_PROTOCOLS_ACME_MAX_NEW_ACCOUNTS_PER_SOURCE |
20 |
Per-source account-creation budget per source window. |
TRSTCTL_PROTOCOLS_ACME_MAX_NEW_ORDERS_PER_SOURCE |
60 |
Per-source order-creation budget per source window. |
TRSTCTL_PROTOCOLS_ACME_SOURCE_WINDOW_SECONDS |
600 |
Source-budget window for ACME nonce/account/order creation. |
TRSTCTL_PROTOCOLS_ACME_NONCE_TTL_SECONDS |
600 |
TTL for unused ACME replay nonces before the request-time janitor drops them. |
TRSTCTL_PROTOCOLS_ACME_STATE_TTL_SECONDS |
86400 |
TTL for pending ACME order/authorization/challenge state before the request-time janitor drops it. |
TRSTCTL_PROTOCOLS_EST_ENABLED / …_TENANT_ID |
false / — |
Serve EST at /.well-known/est/... for the named tenant. |
TRSTCTL_PROTOCOLS_SCEP_ENABLED / …_TENANT_ID |
false / — |
Serve SCEP at /scep for the named tenant. |
TRSTCTL_PROTOCOLS_CMP_ENABLED / …_TENANT_ID |
false / — |
Serve CMP at /cmp for the named tenant. |
TRSTCTL_PROTOCOLS_TSA_ENABLED / …_TENANT_ID |
false / — |
Serve RFC 3161 timestamp evidence for the named tenant through the signer-held timestamping key, instead of minting inventory certificates. |
TRSTCTL_PROTOCOLS_RA_KEY_FILE |
data/protocols/ra-transport.key |
Sealed SCEP/CMP RSA transport identity. Put this on shared persistent storage in HA so replicas use the same cached-client RA material. |
TRSTCTL_PROTOCOLS_TSA_CERT_FILE |
data/protocols/tsa.crt |
Where the TSA's timestamping certificate is persisted, so it stays stable across restarts and is shared across HA replicas. Required when TSA is enabled. |
TRSTCTL_PROTOCOLS_KMIP_ENABLED / …_TENANT_ID |
false / — |
Serve the KMIP mTLS listener for the named tenant. The current served profile supports AES-256 SymmetricKey Create/Get/Locate/Revoke/Destroy. |
TRSTCTL_PROTOCOLS_KMIP_ADDR |
:5696 |
TCP listen address for KMIP. |
TRSTCTL_PROTOCOLS_KMIP_CERT_FILE |
— | PEM server certificate chain for the KMIP listener. Required when KMIP is enabled. |
TRSTCTL_PROTOCOLS_KMIP_KEY_FILE |
— | PEM private key for the KMIP listener certificate. Required when KMIP is enabled. |
TRSTCTL_PROTOCOLS_KMIP_CLIENT_CA_FILE |
— | PEM CA bundle used to verify KMIP client certificates. Required when KMIP is enabled. |
TRSTCTL_PROTOCOLS_SPIFFE_ENABLED / …_TENANT_ID |
false / — |
Serve the SPIFFE Workload API UDS for the named tenant. Requires TRSTCTL_PROTOCOLS_SPIFFE_TRUST_DOMAIN. |
TRSTCTL_PROTOCOLS_SPIFFE_SOCKET_PATH |
/tmp/trstctl-spiffe-workload.sock |
UDS path for the SPIFFE Workload API when enabled. |
TRSTCTL_PROTOCOLS_SPIFFE_TRUST_DOMAIN |
— | SPIFFE trust domain, for example example.org. Required when SPIFFE is enabled. |
TRSTCTL_PROTOCOLS_SSH_ENABLED / …_TENANT_ID |
false / — |
Serve the SSH CA JSON endpoints and KRL for the named tenant. |
Agent mTLS channel
The served agent gRPC channel is a real listener the control plane exposes for steady-state agent heartbeat, renewal, and command fan-out (WIRE-004 / OPS-005) — separate from the one-shot bootstrap enrollment path, which always works. It is off by default; enabling it without a configured signer (the agent CA is custodied there) is a startup error.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_AGENT_CHANNEL_ENABLED |
false |
Mounts the served agent mTLS gRPC channel on TRSTCTL_AGENT_CHANNEL_ADDR. Requires a configured signer. |
TRSTCTL_AGENT_CHANNEL_ADDR |
:9443 |
The agent channel's mTLS gRPC listen address. |
TRSTCTL_AGENT_CHANNEL_HTTP_RENEWAL_ADDR |
:9444 |
Dedicated embedded-client HTTPS renewal listener. Served only when the channel is enabled, uses the same signer-custodied agent CA, and requires a verified agent client certificate. |
TRSTCTL_AGENT_CHANNEL_SERVER_NAME |
empty (loopback SANs only) | DNS SAN the channel's server certificate carries — the name agents pin/verify as their --server-name. Loopback SANs are always added so a co-located agent can verify a localhost connection. |
TRSTCTL_AGENT_CHANNEL_CA_CERT_FILE |
data/ca/agent-ca.crt |
Where the agent CA certificate is persisted, so the agent CA is stable across restarts (an agent's pinned CA does not change on restart). |
TRSTCTL_AGENT_CHANNEL_HEARTBEAT_INTERVAL |
30s |
Next-beat hint returned to agents. |
See Getting started for the blank Compose stack's published agent-channel port and the local CA-pinning steps to reach it from an agent CLI.
WASM plugins
The WASM plugin surface is off by default. When enabled, the binary admits only signed
modules whose detached Ed25519 signature verifies against the configured trusted keys.
CA plugins become external CA entries of type wasm-ca; DNS-provider plugins become a
served ACME DNS-01 provider that tenant provider configs can select; connector plugins
handle matching deployment work from the outbox.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_PLUGINS_ENABLED |
false |
Load signed WASM plugins at startup. |
TRSTCTL_PLUGINS_CA_DIR |
— | Directory containing signed CA plugin pairs: <name>.wasm and <name>.wasm.sig. |
TRSTCTL_PLUGINS_DNS_DIR |
— | Directory containing signed DNS-provider plugin pairs. Each module becomes a served ACME DNS-01 provider that tenant provider configs can select. |
TRSTCTL_PLUGINS_CONNECTOR_DIR |
— | Directory containing signed connector plugin pairs. |
TRSTCTL_PLUGINS_DIR |
— | Legacy connector-plugin directory alias. Ignored when TRSTCTL_PLUGINS_CONNECTOR_DIR is set. |
TRSTCTL_PLUGINS_TRUSTED_KEY_FILES |
— | Comma-separated PEM Ed25519 public keys trusted to sign plugin artifacts. Required when plugins are enabled. |
TRSTCTL_PLUGINS_PINNED_DIGESTS |
— | Optional comma-separated SHA-256 artifact digests that must match admitted modules exactly. |
TRSTCTL_PLUGINS_CAPABILITIES |
— | Comma-separated capabilities granted to loaded plugins, such as fs.write or net.dial. Empty grants no privileged host operation. |
TRSTCTL_PLUGINS_PATH_PREFIXES |
— | Optional comma-separated filesystem prefixes constraining fs.read and fs.write. |
SPIRE upstream authority plugin
When SPIRE should keep serving workload SVIDs but trstctl should own the upstream CA,
configure SPIRE's UpstreamAuthority "trstctl" plugin. SPIRE passes this as HCL
plugin_data to the trstctl-spire-upstream-authority process; these are not trstctl
environment variables.
| Field | Required | Meaning |
|---|---|---|
endpoint |
yes | Base URL of the trstctl control plane, for example https://trstctl.example.com:8443. The plugin calls /api/v1/ca/authorities/{id}/intermediates/csr. |
ca_authority_id |
yes | The trstctl CA authority that signs SPIRE's intermediate CA CSR. |
token_file |
yes | File containing a trstctl API token with certs:issue. Mount it as a secret file readable only by the SPIRE server process. |
common_name |
no | Subject common name for the SPIRE intermediate; defaults to SPIRE Server CA. |
ttl_seconds |
no | Intermediate CA TTL. If SPIRE sends a preferred TTL, the plugin honors SPIRE's value for that mint. |
max_path_len |
no | Path length for the SPIRE intermediate; use 0 so it can sign workload leaves but not another CA below it. |
permitted_dns_domains |
no | Optional DNS name constraints copied into the intermediate CA profile. |
extended_key_usages |
no | Optional extended key usages to request for the intermediate profile. |
idempotency_prefix |
no | Prefix for the stable Idempotency-Key; defaults to spire-upstream. |
Example:
UpstreamAuthority "trstctl" {
plugin_cmd = "/opt/spire/plugins/trstctl-spire-upstream-authority"
plugin_data {
endpoint = "https://trstctl.example.com:8443"
ca_authority_id = "11111111-1111-1111-1111-111111111111"
token_file = "/run/secrets/trstctl-spire-token"
common_name = "SPIRE Server CA"
ttl_seconds = 3600
max_path_len = 0
permitted_dns_domains = ["example.org"]
}
}
Rate limiting
A per-tenant, PostgreSQL-backed rate limiter sheds load on the guarded routes
(429 + Retry-After). See Operations & resilience for the model
and the bulkheads it complements.
ACME also has protocol-local abuse budgets above because its public nonce/account/order routes are not REST API routes. The protocol bulkhead limits concurrent work; the ACME quota knobs bound retained ACME protocol state.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_RATE_LIMIT_ENABLED |
true |
Turn per-tenant rate limiting on/off. |
TRSTCTL_RATE_LIMIT_REQUESTS |
600 |
Burst/budget per window, per tenant. |
TRSTCTL_RATE_LIMIT_WINDOW |
1m |
The refill window (Go duration). |
When enabled, requests must be positive and window a valid positive duration,
or the control plane fails fast at startup.
Bulkheads
Each subsystem runs on its own bounded worker pool. workers caps concurrent
work; queue caps accepted backlog before trstctl rejects fast with structured
backpressure. Every value must be positive. Defaults are conservative for a
single-node or small HA deployment; larger fleets should raise only the subsystem
that is actually saturating.
| Variable | Default | Meaning |
|---|---|---|
TRSTCTL_BULKHEAD_API_WORKERS / TRSTCTL_BULKHEAD_API_QUEUE |
8 / 256 |
Cheap REST/API work. Keep this protected from heavy query, protocol, and agent waves. |
TRSTCTL_BULKHEAD_PROJECTIONS_WORKERS / TRSTCTL_BULKHEAD_PROJECTIONS_QUEUE |
2 / 128 |
Served event-log projection tail ownership and restart work. A saturated projections pool sheds tail starts and retries, while the durable cursor preserves ordered projection. Raise workers only when PostgreSQL and NATS have headroom. |
TRSTCTL_BULKHEAD_OUTBOX_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_QUEUE |
4 / 256 |
Default for every outbox family and the independent “other” lane (ca.*, revocation, DNS automation, ITSM, discovery, and plugin-owned destinations). Existing deployments can keep using only this pair. |
TRSTCTL_BULKHEAD_OUTBOX_EXTERNAL_CA_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_EXTERNAL_CA_QUEUE |
inherits outbox | Override only external-ca.* issuance calls. |
TRSTCTL_BULKHEAD_OUTBOX_CONNECTORS_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_CONNECTORS_QUEUE |
inherits outbox | Override connector.*, including deploy, rollback, test, and right-size calls. |
TRSTCTL_BULKHEAD_OUTBOX_SECRETS_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_SECRETS_QUEUE |
inherits outbox | Override dynamic-secret provider calls (dynsecret.*). |
TRSTCTL_BULKHEAD_OUTBOX_SECRET_SYNC_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_SECRET_SYNC_QUEUE |
inherits outbox | Override outbound secret-sync calls (secret.sync*) without sharing dynamic-secret workers. |
TRSTCTL_BULKHEAD_OUTBOX_MANAGED_KEYS_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_MANAGED_KEYS_QUEUE |
inherits outbox | Override durable managed-key/HSM commands (managedkey.*). |
TRSTCTL_BULKHEAD_OUTBOX_TRANSPARENCY_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_TRANSPARENCY_QUEUE |
inherits outbox | Override transparency publication (transparency.*). |
TRSTCTL_BULKHEAD_OUTBOX_CODE_SIGNING_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_CODE_SIGNING_QUEUE |
inherits outbox | Override code-signing commands (codesign.*) without sharing transparency workers. |
TRSTCTL_BULKHEAD_OUTBOX_NOTIFICATIONS_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_NOTIFICATIONS_QUEUE |
inherits outbox | Override operator notifications (notification.*). |
TRSTCTL_BULKHEAD_OUTBOX_TENANT_SEAL_WORKERS / TRSTCTL_BULKHEAD_OUTBOX_TENANT_SEAL_QUEUE |
inherits outbox | Override the zero-egress tenant seal commit worker (tenantseal.seal). It proves the accepted result is durable before acquiring the cross-replica seal fence. |
TRSTCTL_BULKHEAD_SIGNING_WORKERS / TRSTCTL_BULKHEAD_SIGNING_QUEUE |
4 / 64 |
Control-plane work waiting on signer RPC. Do not set this above signer capacity. |
TRSTCTL_BULKHEAD_QUERY_WORKERS / TRSTCTL_BULKHEAD_QUERY_QUEUE |
4 / 64 |
Heavy graph/risk/read queries that scale with inventory size. |
TRSTCTL_BULKHEAD_POLICY_WORKERS / TRSTCTL_BULKHEAD_POLICY_QUEUE |
4 / 64 |
OPA/Rego policy gate work. Saturation fails closed rather than blocking issuance. |
TRSTCTL_BULKHEAD_PROTOCOLS_WORKERS / TRSTCTL_BULKHEAD_PROTOCOLS_QUEUE |
8 / 256 |
ACME/EST/SCEP/CMP/SPIFFE/SSH/TSA enrollment protocol work. |
TRSTCTL_BULKHEAD_AGENT_WORKERS / TRSTCTL_BULKHEAD_AGENT_QUEUE |
16 / 1024 |
Agent heartbeat and renewal fan-in. Raise this first for large agent fleets. |
TRSTCTL_BULKHEAD_CBOM_WORKERS / TRSTCTL_BULKHEAD_CBOM_QUEUE |
4 / 64 |
CBOM TLS/config scans. Raise only when broad crypto-inventory sweeps are saturating and PostgreSQL/NATS have headroom. |
Fleet-size guidance:
| Shape | Starting point |
|---|---|
| Single-node eval or small production | Use defaults; tune only after trstctl_*_bulkhead_* rejection metrics show pressure. |
| About 1k agents | Increase agent queue first (for example 2048), then agent workers if PostgreSQL/signing have headroom. |
| Very large fleets | Scale agent, protocols, CBOM, and outbox independently; keep API workers modest so operator traffic stays responsive while waves shed elsewhere. |
trstctl --check-config prints the effective bulkheads.<subsystem>.workers and
bulkheads.<subsystem>.queue values, so CI/CD can diff the resolved runtime limits
before a rollout.
Every outbox family owns a separate queue and worker set even when all limits are
inherited from bulkheads.outbox. In plain terms, a connector endpoint that stops
answering can fill only the connector lane; it cannot occupy the workers that issue
through an upstream CA, synchronize a secret, publish code-signing evidence, or page
an operator. JSON config may override a family with outbox_external_ca,
outbox_connectors, outbox_secrets, outbox_secret_sync,
outbox_managed_keys, outbox_transparency, outbox_code_signing, or
outbox_notifications, or outbox_tenant_seal inside bulkheads.
Config file
Any of the above can also be set in a JSON file named by TRSTCTL_CONFIG_FILE;
environment variables override file values, which override defaults.
{
"server": { "addr": ":8443" },
"postgres": { "mode": "external", "dsn": "postgres://..." },
"nats": { "mode": "external", "url": "nats://..." },
"telemetry": { "enabled": false, "instance_id_file": "data/telemetry/instance-id" }
}