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.