Plan and license
trstctl is an MPL-2.0 open-core Machine Identity Security Control Plane. The
product line keeps core credential issuance, enrollment,
rotation primitives, protocol interoperability, audit/export, PostgreSQL RLS
tenant isolation, and the offline license verifier in Free. Enterprise adds the
commercial ee/ feature set. Provider / MSP includes every Enterprise feature and
adds provider-plane operations plus managed-service and resale rights.
Pricing Posture
The canonical billing reference is Pricing; the posture in brief:
Free has no license bill. Enterprise publishes annual USD reference list prices
of $15,000 Standard and $30,000 Plus per production
control_plane_deployment. Provider / MSP publishes wholesale bands of $12,000
for 1–10 managed customers, $30,000 for 11–50, and $72,000 for 51–250; 250+
is negotiated. The MSP controls its downstream hosting and support prices.
Certificates, SVIDs, secrets, API keys, tokens, rotations, and nodes are never
automatic billing units. See Pricing for support and renewal terms.
Console journey
Open Plan and license at /admin/editions to answer two questions first:
which signed features this deployment may use, and when that permission ends.
The opening card shows the current plan, enabled-feature count, expiry, and a
plain-language state such as Community, Active, Grace, or Read-only. The page
makes only the Editions API read needed for that answer.
The deeper proof remains available without putting it in the operator's way:
- Signature verification explains whether the offline Ed25519 signature verified and when the deployment becomes read-only.
- Feature table maps each exact feature ID to its required plan and current mode.
- Entitlement evidence shows the customer, deployment binding, environment, use rights, FIPS posture, and optional packaging evidence. Deployment distribution and active-active issuance evidence are fetched only after the nested architecture disclosure is opened.
Add license is an operator guide, not a browser upload. A private license is
installed as an operator-controlled 0600 file, bound to the signed deployment
ID and environment, then supplied to both the control plane and isolated signer
at startup. Restart those two processes together. This avoids browser custody of
the license and prevents the control plane and signer from temporarily applying
different rights. Read or verification failures are shown with sanitized,
fail-closed language and a safe retry; raw server errors are not displayed.
Where the trusted vendor key comes from
A license verifies only against the vendor's license signing public keys baked
into the binaries at build time. Native builds bake them with
make LICENSE_KEYS_B64=$(base64 -w0 license-signing.pub); container images bake
them with the LICENSE_KEYS_B64 build argument of deploy/docker/Dockerfile
(the release pipeline passes the repository variable
TRSTCTL_LICENSE_KEYS_B64). Only public keys are ever baked. A build without
a baked key bakes no trust: no license file can verify and the deployment stays
Community, which is the fail-closed default for local builds. The local
click-through demo is the one exception: its image target mints a throw-away
key and license inside the build and deletes the signing material, so a demo
license never verifies on any other build.
Containers and the partner lab
In containers the license is a read-only mounted file owned by the operator, never an image layer. The partner lab ships this path as an opt-in profile:
TRSTCTL_LAB_LICENSE_FILE=/secure/acme-license.json \
TRSTCTL_LAB_LICENSE_KEYS_B64="$(base64 -w0 vendor-ed25519.pub)" \
TRSTCTL_LAB_LICENSE_DEPLOYMENT_ID=acme-lab \
TRSTCTL_LAB_LICENSE_ENVIRONMENT=non_production \
deploy/demo/lab/run.sh
run.sh refuses a license file that is group- or world-readable, builds the
clean release image target with the vendor key baked in, copies the file once
into the run-owned runtime volume for the service user, and starts the control
plane and the isolated signer with the same bound deployment ID and
environment. The deployment ID must be one the license signed for that
environment; a copied file with another binding fails startup. The same profile
pins the lab's local identity provider for provider-operator sign-in (see
Provider operator delegation and the
provider journey).
Buyer Matrix
| Packaging line | Free | Enterprise | Provider / MSP |
|---|---|---|---|
| Buyer | Organization operating trstctl for itself | Organization needing the commercial feature set | MSP operating or reselling trstctl-backed services to customers |
| Primary billing unit | None | Per control-plane deployment | Negotiated managed-customer band |
| Core protocols | ACME, EST, SCEP, CMP, SPIFFE, SSH CA, TSA | Included | Included |
| Tenant isolation | PostgreSQL RLS and event spine | Included | Included; shared multi-tenant control plane is the normal shape |
| Enterprise features | Not included | FIPS artifact posture, remediation, PQC, HA support, BYOK, governance, PCAS, agent delegation, reconciliation, and VDEC | All Enterprise features |
| Provider operations | Not included | Not included | Provider plane, metering, white label, and siloed isolation |
Product motion and commercial ee/ rights |
Self-hosted core under MPL-2.0 | Self-hosted commercial feature set | Self-host, managed service, and resale of the commercial feature set |
| Deployment flexibility | Customer operated | Customer operated | Shared control plane or dedicated customer deployments |
| Pricing | No license fee | $15,000 Standard or $30,000 Plus annual reference list | $12,000 / $30,000 / $72,000 annual wholesale reference bands; 250+ negotiated |
| Environment entitlement | Community deployments are unmetered | 1 production + 3 signed non-production deployment slots | Same bundle per licensed Provider control plane |
The same binary lineage serves all three tiers. The offline signed tier drives both feature inheritance and use rights. Core multi-tenancy, audit/export, crypto, and the license verifier remain in core.
Signed deployment environment entitlement
New licenses use claim version 2. Their signed environment_entitlement object
contains production_deployment_id, up to three
non_production_deployment_ids, and non_production_allowance: 3. The vendor
helper refuses duplicate IDs, a production ID repeated as non-production, an
unsafe ID, or more registered non-production IDs than the allowance.
The offline vendor helper creates that bundle explicitly:
trstctl-license sign \
--private-key vendor-ed25519.key \
--out acme-license.json \
--id lic-acme-2027 --customer "Acme Corp" --tier enterprise \
--production-deployment-id acme-prod \
--non-production-deployment-ids acme-stage,acme-dev,acme-test \
--expires-at 2027-08-13T00:00:00Z
The running binary binds those claims to operator-owned configuration:
TRSTCTL_LICENSE_FILE=/etc/trstctl/license.json \
TRSTCTL_LICENSE_DEPLOYMENT_ID=acme-stage \
TRSTCTL_LICENSE_ENVIRONMENT=non_production \
trstctl
Both runtime values are required together for v2. production must match the
one signed production ID; non_production must match one signed non-production
ID. A copied file with a changed or misclassified ID fails startup. The Editions
API exposes deployment_entitlement.environment, deployment_id,
production_units_consumed, registered and remaining non-production slots, and
the legacy marker. A non-production deployment therefore provides a
machine-readable zero production-unit result rather than relying on sales prose.
Version 1 files remain readable for upgrade continuity, but they are explicitly
production-only and legacy_unbound: true; a v1 file cannot activate the
bundled non-production right. This compatibility rule avoids inventing an
entitlement that was never signed.
The supervised signer child receives the same deployment ID and environment as
the control plane and independently applies the same bound loader. An operator
starting trstctl-signer as a separate process must pass
--license-deployment-id and --license-environment beside --license; a v2
file without that pair fails closed before the signing service listens.
Core Protocols
These protocol surfaces are Free/Community capabilities. They are not Enterprise-only
features in internal/license.
| Capability | Edition | Notes |
|---|---|---|
| ACME | Community | ACME server, account/order flow, ARI, and DNS-validation framework. |
| EST | Community | RFC 7030 enrollment endpoint. |
| SCEP | Community | RFC 8894 enrollment endpoint. |
| CMP | Community | RFC 4210 / CMPv3 enrollment endpoint. |
| SPIFFE Workload API | Community | X.509 and JWT SVID workload identity surface. |
| SSH CA | Community | SSH certificate authority endpoints and KRL publication. |
| TSA | Community | Timestamping authority surface. |
License-Gated Features
This table mirrors internal/license exactly. A feature absent from this table is
Free/Community by default unless a signed license explicitly grants it as an extra.
Provider inherits every Enterprise row below, then adds the Provider rows.
| Feature ID | Edition | Product line |
|---|---|---|
fips |
Enterprise | Assurance: FIPS-capable distribution posture and evidence. |
remediation |
Enterprise | Governance: guided remediation workflows and controls. |
pqc |
Enterprise | Proprietary post-quantum algorithms, key/certificate types, issuance/signing paths, migration APIs/UI, and tests. |
ha_support |
Enterprise | Scale: served enterprise support posture, SLA target catalog, 24x7 production tier, and professional-services packages. |
byok |
Enterprise | Assurance: bring-your-own-key / external custody operations. |
governance |
Enterprise | Governance: advanced approvals, policy, and audit controls. |
pcas |
Enterprise | Proof-carrying algorithm succession. |
agent-delegation |
Enterprise | Chain-bound AI agent identity lifecycle enforcement. |
reconcile |
Enterprise | Cross-plane trust reconciliation rounds and evidence machinery. |
vdec |
Enterprise | Verifiable decommissioning dependency-state re-protection and destruction proof machinery. |
provider_plane |
Provider | Managed-provider control plane features, gated by per-customer delegation (see below). |
metering |
Provider | Provider usage metering, durable per-customer, pullable as invoice evidence (see below). |
white_label |
Provider | Provider branding controls. |
siloed_isolation |
Provider | Provider tenant-silo operating mode. |
Provider operator delegation
The provider plane has two independent gates, and both fail closed.
Authentication answers who an operator is. With no Authenticator
configured, /provider/ refuses every request; there is deliberately no
placeholder verifier.
Delegation answers which customers that operator may touch, and through
which operations. Grants live in provider_operator_delegations and are read on
every customer-scoped action:
- An operator acts only on customers explicitly granted to them. Naming any other customer is refused, and the refusal happens before the store is written — a suspended customer and an audited refusal must never disagree.
- Operations are granted separately (
read,provision,suspend,resume,offboard,break-glass) because they carry different blast radii. Suspending interrupts a live service and is reversible; offboarding destroys. Being trusted with one is not being trusted with the other. - There is no wildcard customer. A wildcard grant is indistinguishable from the unscoped access this replaces.
- The tenant list returns only delegated customers. The full roster is the provider's commercial information, and it is the map an operator would need to attempt a cross-customer action.
- Break-glass is re-checked when the grant is USED, not only when it was requested, so revoking a delegation stops access an operator already holds.
- No delegation source, or a delegation source that cannot be read, refuses everything. Failing open on a read error would make the plane widest exactly when it is least healthy.
Grants are minted with a local subcommand against PostgreSQL and the event
log. Name the customer by the slug you will provision (its tenant id is
derived from the slug, so a customer can be delegated before it exists; the
command prints the derived id) or by an existing tenant id. Every invocation
needs a stable idempotency key; an identical retry returns the canonical
authority event, while reusing the key for a changed grant is refused. The same
rule holds for every provider mutation over the API: a key binds its first
answer, a refusal included, and a replayed answer carries the
Idempotent-Replayed: true header. After you fix the cause of a refusal, retry
with a new key:
trstctl provider-grant -operator op-1 -customer acme \
-operations read,provision,suspend -granted-by platform-admin \
-idempotency-key acme-op-1-read-provision-suspend-v1
# time-boxed: the grant stops authorizing after -expires-at (RFC3339)
trstctl provider-grant -operator op-1 -customer acme \
-operations read -granted-by platform-admin \
-expires-at 2027-01-01T00:00:00Z \
-idempotency-key acme-op-1-read-until-2027-v1
trstctl provider-grant -operator op-1 -customer acme \
-operations offboard -revoke -granted-by platform-admin \
-idempotency-key acme-op-1-offboard-revoke-v1
Offboarding a customer clears every grant over it. Tenant ids are derived from the customer slug, so a grant that outlived the tenancy would hand a reused slug to whoever held access on the old customer. This applies to database-backed grants; a deployment still keeping grants in a static configuration file must remove those lines itself, since rewriting an operator's config file from the running process would put the file out of step with the system it describes.
This is a local command rather than a served route because of the bootstrap
problem: a route that hands out provider authority must itself be authorised by
somebody holding provider authority, and at install time no such operator
exists. The command opens the configured PostgreSQL and JetStream stores,
bootstraps any pre-event delegation rows exactly once, appends one immutable
provider.delegation.granted or provider.delegation.revoked event, and lets
the provider authority projection update the delegation view. It never writes
the table directly. When embedded NATS is configured, run the command while the
control plane is offline so two processes do not open the same file-backed
store.
Provider OIDC bearer verification uses pinned JWKS, issuer, audience, expiry,
not-before, role, and MFA claims. Provider SAML is a separate SP at
/provider/v1/auth/saml/{login,acs,metadata}: the crypto boundary validates the
signed assertion, issuer, audience/recipient, time window, request correlation,
role, and MFA attributes before minting a Provider-only HttpOnly session. That
cookie never becomes a customer-tenant session; state-changing requests also
need its double-submit X-Provider-CSRF-Token value.
Provider SCIM is served at /provider/scim/v2. Token bytes are read from
custody-checked files, hashed, wiped, and never retained as strings. User
join/update/active:false/delete emits immutable operator lifecycle events.
When SCIM is enabled, OIDC and SAML re-read that projected operator row on
every request. A still-valid token or browser session therefore stops at the
first request after a leaver event; the leaver projection also revokes every
standing customer grant for that operator.
Provider administrators with current MFA use GET /provider/v1/operators,
GET /provider/v1/access/customers, and the per-operator /delegations,
/revocations, and /role mutation routes.
The console shows the operator, SCIM source, role, exact customer/operation,
grant source, expiry, last use, and retained revocation evidence. These routes
are not the install-time bootstrap: the local provider-grant command remains
available for creating the first authority before an administrator exists.
POST /provider/v1/auth/logout revokes the Provider session, clears its cookie,
and is protected by the same CSRF and idempotency contract as other mutations.
Provider authority event source and retries
Customer lifecycle, delegation, quota, white-label branding, and break-glass
grant/use state share one tenant-scoped immutable authority history. The
provider_tenants, provider_operators, provider_operator_delegations,
provider_tenant_quotas, tenant_branding, and
provider_breakglass_grants tables are read models owned only by that
projection; their PostgreSQL stores expose no direct mutators. An upgraded
installation captures each uncovered pre-event row once before its first
rebuild, including brand token overrides and both break-glass consents.
Every state-changing /provider/v1 request requires Idempotency-Key. The key
is immutably bound to the authenticated operator, method, path, and body; exact
retries return the original status, headers, and bytes, including concurrent
retries, while a changed command returns 409. Break-glass result access
increments use count and is therefore POST /provider/v1/breakglass/{grant}/results, not a read-looking GET. The provider
console generates a distinct key for every mutation it submits.
GET /provider/v1/activity?limit=100 derives a newest-first authority evidence
view directly from that same immutable history. It returns event identity,
sequence, type, time, customer, actor, grant, subject, and reason only: request
bindings, authority state payloads, and break-glass tenant snapshots are never
exposed through the history route. Current delegation is applied before any
customer event is returned, so an operator cannot discover another customer's
authority history; deployment-wide isolation-drill evidence is Provider-admin
only. The Provider console renders this view beside the controls, making the
evidence for a completed mutation visible without trusting a second audit store.
Usage metering and invoice evidence
Metering is durable: usage is recorded in PostgreSQL under the same row-level security fence as every other tenant table, so it survives a restart and cannot be read across tenancies.
GET /api/v1/provider/usage-evidence (CLI: trstctl usage evidence, console:
Platform → Usage & invoice evidence) returns the evidence document for a period.
Two properties of that document matter more than the totals:
- The tenant route is scoped to the caller's own tenancy. A
customer_idnaming any other tenant is refused with 403 before the store is touched. Provider billing staff do not impersonate that tenant: their separate workforce credential callsGET /provider/v1/tenants/{id}/usage-evidencewith an exact customer read delegation (thereadoperation). An undelegated customer or a grant for another operation is refused before the customer's forced-RLS metering transaction opens. - It says whether it may be billed.
signableis false, with areason, for any period the metering store cannot vouch for end to end — an open period, metering that was not durable for the whole window, or coverage that starts after the period does. The totals are still returned, because "your usage is incomplete and here is how" is actionable and an error is not, but they are a partial view rather than an invoice. There is deliberately no way to get the numbers without the verdict attached.
An absent metering store returns 503 rather than a zero-usage document: no metering and no usage are different facts.
The Provider route returns the same canonical signed JSON/JWS document as the
tenant route, or ?format=csv for a strict finance CSV whose rows retain the
customer, period, signable verdict, reconciliation result, and document digest.
The /provider console selects customer and period, downloads signed JSON or
finance CSV, and fetches public trust separately from GET /provider/v1/evidence/verification-keys. It reconstructs the displayed
document's canonical bytes and shows Signature verified only when RS256,
the protected billing-invoice artifact domain, key id, digest, and signature
all match. A green label therefore does not trust the evidence response's own
signable boolean.
The same customer selection also reads GET /provider/v1/tenants/{id}/health. This is a small operational view, not
break-glass access: after the exact read delegation succeeds,
DirectTenantSnapshot enters that customer's PostgreSQL RLS identity and
counts only active certificate rows for that tenant. The response distinguishes
healthy, suspended, offboarded, and no-active-certificate states. A customer
that does not exist returns 404; a storage failure returns 503; and the console
renders both as unknown/unavailable rather than showing zero as if it were a
measured fact. Customer health and Invoice evidence therefore name the
same selected customer while keeping lifecycle/count truth separate from the
signed billing period.