A ctlplne studio product
trstctl /docs GitHub ↗ Live demo

Install

trstctl has two binaries you install depending on the role:

  • Control plane (trstctl) — the API, web UI, orchestrator, and event spine. In single-node mode it also supervises the isolated signing service (trstctl-signer) as a child process.
  • Agent (trstctl-agent) — runs inside your network to discover, deploy, and monitor credentials on a host.

Pick the platform you are installing on.

Docker (control plane)

The published image is distroless and unprivileged, but a single docker run is not a complete production deployment. A real control plane also needs a durable PostgreSQL database, replicated JetStream, persistent signer custody, stable TLS and key-encryption material, and an independent signer authorization provider. A disposable container would lose or misrepresent those boundaries.

Use the supported blank Compose stack for a local evaluation. It starts the separate signer, PostgreSQL, NATS, local login, persistent named volumes, and the browser trust-publication path together:

docker compose -f deploy/docker/docker-compose.yml up --build --detach --wait --wait-timeout 180

For a pre-populated click-through environment, use deploy/demo/docker-compose.yml. Both Compose files are intentionally local evaluation profiles, not production templates. Use the Helm path below for a production control plane; it fails closed when a required security input is missing.

Pin and verify every published image before either deployment path uses it:

export TRSTCTL_IMAGE_REF='ghcr.io/ctlplne/trstctl@sha256:<release-image-digest>'
scripts/verify-image.sh "$TRSTCTL_IMAGE_REF"

Verify a published image before you run it — its keyless cosign signature and its CycloneDX SBOM attestation — with the helper:

scripts/verify-image.sh "$TRSTCTL_IMAGE_REF"

That wraps the underlying cosign check (only an image built by this repo's release workflow verifies):

cosign verify "$TRSTCTL_IMAGE_REF" \
  --certificate-identity-regexp '^https://github.com/.*/trstctl/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

See Supply chain for the full signing, SBOM, provenance, and dependency-scanning story. Each tagged release also includes SLSA provenance named trstctl-container-and-manifest.intoto.jsonl; verify that file against the published container digest and rendered Kubernetes agent manifest when you mirror artifacts into your own registry. For Kubernetes admission-time enforcement, start from deploy/kubernetes/sigstore-policy.yaml; it admits only digest-pinned trstctl images signed by this repository's release workflow identity.

For disconnected networks, use the air-gapped install path. It builds a transfer bundle with the release image, Helm chart, values-airgap.yaml, checksums, and offline docs, then enables the no-phone-home egress guard in the running control plane.

Kubernetes (control plane via Helm)

The control plane installs with the Helm chart under deploy/helm/trstctl. It deploys the API/UI with the signing service isolated as a locked-down sidecar with no network listener (it talks to the control plane only over a shared in-memory socket, so private keys stay in their own process), against external PostgreSQL and NATS, behind a default-deny NetworkPolicy, with TLS on by default:

Production-style external NATS requires an independent signer authorization provider plus an operator-provided signer authorization client. The official trstctl image does not include that organization-specific executable. Build a derived control-plane image that adds only your small client at /opt/trstctl-auth/bin/signer-token-provider, pin that derived image by digest, and operate the approval provider outside the control-plane process. The client receives one sign-intent JSON document on stdin and returns one base64 authorization token on stdout. It may call your approval service, but it must not contain the signer's verifier secret.

helm install trstctl deploy/helm/trstctl \
  --namespace trstctl --create-namespace \
  --set image.repository='registry.example/trstctl-with-auth-client' \
  --set image.digest='sha256:<release-image-digest>' \
  --set postgres.dsn='postgres://user:pass@pg-host:5432/trstctl?sslmode=require' \
  --set nats.url='nats://nats-host:4222' \
  --set kek.existingSecret=trstctl-kek \
  --set signer.auth.tokenCommand=/opt/trstctl-auth/bin/signer-token-provider

For a single-replica evaluation only, the co-resident authorizer is an explicit weaker topology. It is accepted only together with single-replica NATS:

helm install trstctl-eval deploy/helm/trstctl \
  --namespace trstctl --create-namespace \
  --set image.digest='sha256:<release-image-digest>' \
  --set postgres.dsn='postgres://user:pass@pg-host:5432/trstctl?sslmode=require' \
  --set nats.url='nats://nats-host:4222' \
  --set nats.replicas=1 \
  --set nats.allowSingleReplica=true \
  --set kek.generate=true \
  --set signer.auth.allowCoResidentAuthorizer=true
kubectl -n trstctl rollout status deploy/trstctl
kubectl -n trstctl exec deploy/trstctl -c trstctl -- /usr/local/bin/trstctl --ready-check

Open the production console through the hostname covered by the certificate in tls.existingSecret. A localhost port-forward does not change certificate names; do not disable TLS verification to make that mismatch disappear.

The release pipeline also publishes the packaged chart as a cosign-signed OCI artifact to GHCR, so you can verify the chart's provenance before installing — the same keyless-OIDC identity that signs the image:

cosign verify ghcr.io/ctlplne/trstctl/charts/trstctl:<chart-version> \
  --certificate-identity-regexp '^https://github.com/.*/trstctl/.github/workflows/release.yml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

For offline or mirrored installs, also download trstctl-helm-chart.intoto.jsonl from the GitHub Release and verify the packaged chart bytes with slsa-verifier. For a full no-phone-home Kubernetes install, use deploy/helm/trstctl/values-airgap.yaml as described in Air-gapped install; it turns on TRSTCTL_AIRGAP_ENABLED and scopes datastore egress to operator-owned private CIDRs.

See deploy/helm/trstctl/README.md in the source checkout for the full values reference. The chart runs the signer co-located (sidecar, over an in-memory UDS) by default; set signer.mode=isolated plus the required signer.mtls.* values to render a separate signer pod reached over mutually pinned mTLS (TLS 1.3, both-ways certificate pinning). A focused Kubernetes Operator binary (cmd/trstctl-operator) ships for CRD-driven Deployment reconciliation: replicas, image, PostgreSQL DSN Secret reference, NATS URL/replica knobs, sidecar-signer socket/volumes, and managed-key provider enablement. Its manifest runs two replicas with real leader election through a Kubernetes Lease, so one replica reconciles and the other stays hot. Helm remains the supported full control-plane install for services, ingress, generated secrets, network policy, and cross-pod signer mTLS topology — see limitations.

Install the exact API client

trstctl-cli is the standalone client for people, scripts, CI jobs, and customer AI agents. Each tagged release publishes Linux, macOS, and Windows archives, a machine-readable manifest, one SHA256SUMS file, and SLSA provenance named trstctl-cli.intoto.jsonl. The client archive contains no token and no CA bundle. You choose both when you connect it to a self-hosted control plane.

This Linux example downloads one exact release, verifies the archive and manifest, then checks that the program reports the same full 40-character source commit as the manifest. Replace the version and platform deliberately; do not use a moving latest URL in automation.

version=0.5.4
platform=linux_amd64
base="https://github.com/ctlplne/trstctl/releases/download/v${version}"
archive="trstctl-cli_${version}_${platform}.tar.gz"
manifest="trstctl-cli_${version}_manifest.json"
checksums="trstctl-cli_${version}_SHA256SUMS"

curl --fail --location --proto '=https' --tlsv1.2 --remote-name "${base}/${archive}"
curl --fail --location --proto '=https' --tlsv1.2 --remote-name "${base}/${manifest}"
curl --fail --location --proto '=https' --tlsv1.2 --remote-name "${base}/${checksums}"
grep -E " (${archive}|${manifest})$" "$checksums" | sha256sum -c -

tar -xzf "$archive"
expected_commit="$(jq -r .source_commit "$manifest")"
case "$expected_commit" in (*[!0-9a-f]*|'') exit 1;; esac
[ "${#expected_commit}" -eq 40 ]
trstctl-cli --version | grep -F "commit ${expected_commit}"
sudo install -m 0755 trstctl-cli /usr/local/bin/trstctl-cli

Before the first authenticated request, inspect the server certificate and set the public trust bundle explicitly. TRSTCTL_CA_FILE never disables TLS verification. Put the scoped API token in TRSTCTL_TOKEN only for the process that needs it; do not place it in shell history, an archive, or the manifest.

export TRSTCTL_SERVER=https://control.example:8443
export TRSTCTL_CA_FILE=/path/to/inspected-control-plane-ca.pem
read -r -s TRSTCTL_TOKEN
export TRSTCTL_TOKEN
trstctl-cli capabilities list

For a safely resumable mutation, choose and retain one explicit idempotency key; retrying the same request with that key returns the original result instead of performing the change twice. Unset the token when the task finishes.

trstctl-cli --idempotency-key owner-payments-bootstrap owners create -f owner.json
unset TRSTCTL_TOKEN

Kubernetes (agent)

The trstctl agent runs as a DaemonSet so every node is covered. The manifests live under deploy/kubernetes (namespace, RBAC, and the DaemonSet):

helm upgrade --install trstctl deploy/helm/trstctl \
  --namespace trstctl --create-namespace \
  --set agentChannel.enabled=true \
  --set agentChannel.publicAddress=trstctl:9443 \
  --set agentChannel.serverName=trstctl

export TRSTCTL_AGENT_IMAGE='ghcr.io/ctlplne/trstctl@sha256:<release-image-digest>'

umask 077
bootstrap_token_dir="$(mktemp -d)"
rendered_agent_daemonset="$(mktemp)"
trap 'rm -rf "$bootstrap_token_dir" "$rendered_agent_daemonset"' EXIT

kubectl apply -f deploy/kubernetes/namespace.yaml
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' |
  while IFS= read -r node; do
    [ -n "$node" ] || continue
    jq -nc --arg allowed_identity "$node" '{allowed_identity:$allowed_identity}' |
      trstctl-cli agents enroll-token -f - | jq -r .token > "$bootstrap_token_dir/$node"
  done
kubectl -n trstctl create secret generic trstctl-agent-bootstrap \
  --from-file="$bootstrap_token_dir" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n trstctl create secret generic trstctl-cert-manager-issuer \
  --from-literal=signer-url="https://trstctl:8443/api/v1/ca/authorities/<ca-authority-id>/issue" \
  --from-literal=token="$TRSTCTL_TOKEN" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n trstctl create configmap trstctl-ca-bundle \
  --from-file=ca-bundle.pem=/path/to/agent-channel-ca.pem \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deploy/kubernetes/certmanager-issuer-crds.yaml
kubectl apply -f deploy/kubernetes/rbac.yaml
scripts/release/render-kubernetes-agent-daemonset.sh "$TRSTCTL_AGENT_IMAGE" > "$rendered_agent_daemonset"
kubectl apply -f "$rendered_agent_daemonset"

The DaemonSet points at the in-namespace trstctl Service and reads a single-use bootstrap token from Secret/trstctl-agent-bootstrap. That Secret must have one key per node, with each key named exactly like the node's metadata.name; the DaemonSet mounts only the matching key with subPathExpr: $(NODE_NAME). The minted token is pinned to that same node name with allowed_identity, and enrollment rejects any CSR whose common name or identity SANs ask for a different agent identity. The DaemonSet also sets --server-name=trstctl, so the Helm value above is required for the agent-channel certificate SAN. TRSTCTL_AGENT_IMAGE must be an immutable .../trstctl@sha256:<release-image-digest> reference; the render script refuses tags and the all-zero placeholder. Create ConfigMap/trstctl-ca-bundle with ca-bundle.pem before applying the rendered DaemonSet; the agent uses that bundle to pin bootstrap HTTPS before posting the one-time token and to verify the steady-state mTLS channel. The agent identity key and certificate live on the node at /var/lib/trstctl-agent through a hostPath volume, so pod replacement does not spend a new bootstrap token. The DaemonSet initContainer prepares that host directory for the non-root agent uid using the same shipped trstctl-agent binary; remove that host directory only for intentional re-enrollment. If you use cert-manager, install the trstctl Issuer/ClusterIssuer CRDs and create Secret/trstctl-cert-manager-issuer: signer-url is the served trstctl issuance endpoint, and token is mounted as a file so it is never placed in pod arguments or environment variables. See deploy/kubernetes/README.md for the exact env and Secret wiring.

Linux (control plane or agent)

Install from a release binary or build from source.

From source (requires Go 1.26.6+):

git clone https://github.com/ctlplne/trstctl
cd trstctl
make build           # builds all eight repository commands into ./bin
sudo install -m 0755 bin/trstctl /usr/local/bin/trstctl
sudo install -m 0755 bin/trstctl-agent /usr/local/bin/trstctl-agent

make build produces all eight repository commands in ./bin: trstctl, trstctl-signer, trstctl-agent, trstctl-cli, trstctl-operator, trstctl-license, terraform-provider-trstctl, and trstctl-spire-upstream-authority. Install only the roles this host needs; do not place the vendor-side license signer on a control-plane host.

Run the agent under systemd so it restarts on failure and on boot. A minimal unit:

# /etc/systemd/system/trstctl-agent.service
[Unit]
Description=trstctl agent
After=network-online.target

[Service]
ExecStart=/usr/local/bin/trstctl-agent
Restart=on-failure
User=trstctl

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now trstctl-agent

macOS (agent)

Build the agent (or download the macOS release) and run it as a launchd agent.

make build
sudo install -m 0755 bin/trstctl-agent /usr/local/bin/trstctl-agent

Create a launchd job at /Library/LaunchDaemons/io.trstctl.agent.plist with a ProgramArguments entry of /usr/local/bin/trstctl-agent and KeepAlive set, then load it:

sudo launchctl load /Library/LaunchDaemons/io.trstctl.agent.plist

The agent installs certificates into the login/keychain destinations you configure and never moves private keys off the host.

Windows (agent)

On Windows the agent runs as a Service Control Manager (SCM) service and installs certificates into the Windows certificate store (CryptoAPI / CNG). Build the MSI:

make dist-windows     # cross-compiles trstctl-agent.exe and packages the MSI

make dist-windows Authenticode-signs both the .exe and the .msi only when WINDOWS_CODESIGN_URL points at the remote signing service used by the release pipeline; otherwise it builds them unsigned and says so. The official agent-windows release job runs in the protected windows-code-signing environment, authenticates to that remote signer with GitHub OIDC, and verifies the Authenticode signature before upload. No long-lived code-signing PKCS#12 is decoded or written on the CI runner. That means any published Windows agent artifact from the release pipeline is Authenticode-signed.

Install it (elevated PowerShell):

$token = (trstctl-cli agents enroll-token | ConvertFrom-Json).token
Set-Content -Path C:\ProgramData\trstctl\bootstrap-token.txt -Value $token -NoNewline

msiexec /i trstctl-agent.msi /qn `
  ENROLLURL=https://cp:8443 `
  SERVER=cp:9443 `
  SERVERNAME=cp `
  CABUNDLE=C:\ProgramData\trstctl\ca-bundle.pem `
  BOOTSTRAPTOKENFILE=C:\ProgramData\trstctl\bootstrap-token.txt

The MSI registers and starts the service only after the first-boot settings are present: enrollment base URL, bootstrap token file, CA bundle, agent-channel endpoint, and server name. The token is single-use; after the service enrolls and persists its certificate, rotate or delete the file. See deploy/windows/README.md for Authenticode signing, direct service install, and the WiX/msitools build details.

Verify the agent download

Before installing a downloaded agent, authenticate it. On Windows, confirm the Authenticode signature and inspect the signer:

Get-AuthenticodeSignature .\trstctl-agent.msi   # Status must be 'Valid'

The protected release workflow publishes the signed Windows agent files as durable GitHub Release assets: trstctl-agent.exe, trstctl-agent.msi, and SHA256SUMS. On any platform you can verify the published checksums against those release assets, and (when present) the signature with osslsigncode:

sha256sum -c SHA256SUMS                    # the agent .exe/.msi hashes match the release
osslsigncode verify -in trstctl-agent.msi # reports a valid Authenticode signature

The control-plane and agent container image is additionally cosign-signed; see "Verify a published image" above for scripts/verify-image.sh / cosign verify.

Verify the install

On any platform:

trstctl --version
trstctl -check-config        # prints the effective configuration; non-zero on a bad config

Use --version to inspect a daemon's build and --help to see its flags. The control plane, agent and signer reject unexpected positional arguments before loading runtime configuration or starting services. For example, trstctl version fails with usage guidance; trstctl --version prints the build and exits. Supported administration commands such as trstctl token create keep their own command syntax.

Next: Configuration to point trstctl at your datastores, then Getting started to issue a certificate. To remove trstctl, see Uninstall.

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