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. Run it against your datastores by digest, after verifying the release image:
export TRSTCTL_IMAGE_REF='ghcr.io/ctlplne/trstctl@sha256:<release-image-digest>'
docker run --rm -p 8443:8443 \
-e TRSTCTL_POSTGRES_MODE=external \
-e TRSTCTL_POSTGRES_DSN='postgres://user:pass@db:5432/trstctl?sslmode=require' \
-e TRSTCTL_NATS_MODE=external \
-e TRSTCTL_NATS_URL='nats://nats:4222' \
"$TRSTCTL_IMAGE_REF"
For a self-contained blank evaluation that brings up Postgres and NATS for
you, use the Compose stack from Getting started
(deploy/docker/docker-compose.yml); for a pre-populated demo with local SSO,
the demo stack (deploy/demo/docker-compose.yml).
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
command. The example assumes your control-plane image contains the executable at
/usr/local/bin/trstctl-sign-approve; replace that path with your independently
operated provider. It receives one sign-intent JSON document on stdin and returns
one base64 authorization token on stdout. Do not give that provider's verifier
secret to the control plane.
helm install trstctl 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 kek.existingSecret=trstctl-kek \
--set signer.auth.tokenCommand=/usr/local/bin/trstctl-sign-approve
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 port-forward svc/trstctl 8443:8443 # https://localhost:8443 (-k)
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
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.
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.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.5+):
git clone https://github.com/ctlplne/trstctl
cd trstctl
make build # builds ./bin/trstctl, trstctl-signer, and trstctl-agent
sudo install -m 0755 bin/trstctl /usr/local/bin/trstctl
sudo install -m 0755 bin/trstctl-agent /usr/local/bin/trstctl-agent
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
Next: Configuration to point trstctl at your datastores, then Getting started to issue a certificate. To remove trstctl, see Uninstall.