Runner Helm chart
The runner is the part of TruStacks that runs in your cluster. You install it from a signed OCI Helm chart:
oci://ghcr.io/trustacks/charts/runnerThe published chart has the hosted defaults baked in: SaaS mode on, the
control plane at https://api.trustacks.com, and the runner image of the same
version pinned. You add one value, your enrollment token.
Requirements: Kubernetes 1.28 or later, Helm 3.14 or later (for
--reset-then-reuse-values), and outbound HTTPS from the runner’s namespace.
Install
Generate an enrollment token under Runners and copy the command the page shows. It has this shape; the commands on this page use three shell variables, and the app’s command has the version and token filled in:
NAMESPACE=trustacks-system # any namespace you choose
VERSION=0.0.0 # placeholder: the published version the app shows
TOKEN=paste-from-ui # placeholder: the enrollment token the app shows
helm install trustacks-runner oci://ghcr.io/trustacks/charts/runner \
--version "$VERSION" \
-n "$NAMESPACE" --create-namespace \
--set saas.enrollmentToken="$TOKEN"Copy the command from the app rather than from this page. The app fills in the version the hosted control plane currently advertises. See Versioning.
Then watch it start and enroll. With the release name trustacks-runner, the
deployment is trustacks-runner-runner:
kubectl -n "$NAMESPACE" rollout status deploy/trustacks-runner-runner
kubectl -n "$NAMESPACE" logs deploy/trustacks-runner-runner | grep -i enrollThe Runners page shows the runner as connected once it has enrolled.
The enrollment token
-
Reusable, does not expire. The runner presents it again each time it renews its session, about every 15 minutes. Treat it like a password.
-
Revoking it. Revoke and generate a new token on Settings → Runners revokes every token in the workspace and mints one replacement. A revoked token can never enroll a new runner. Runners on 0.2.14-rc26 or later keep renewing on their own registered key (the session is sealed to that key); older runners disconnect at their next renewal until they get the new token. The app gives you the command:
NEW_TOKEN=... # the replacement token the app showed you once helm upgrade trustacks-runner oci://ghcr.io/trustacks/charts/runner \ --version "$VERSION" \ -n "$NAMESPACE" --reset-then-reuse-values \ --set saas.enrollmentToken="$NEW_TOKEN"If you manage the Secret yourself, update the key named by
saas.enrollmentTokenSecret.key(enrollment-tokenby default) instead. The chart mounts the Secret as a volume, so a running pod reads the new value at its next renewal. Deleting a runner on the Runners page does not retire the token, and while the token is valid the deleted runner enrolls again at its next renewal. To cut off one runner, revoke, then delete it. See Revoke enrollment tokens. -
Where it lives. With
--set saas.enrollmentToken=...the chart creates the Secrettrustacks-runner-enrollmentfor you, and the token is also stored in the Helm release. To keep it out of the release, create the Secret yourself and leavesaas.enrollmentTokenempty:kubectl -n "$NAMESPACE" create secret generic trustacks-runner-enrollment \ --from-literal=enrollment-token="$TOKEN"
Values
The values you are likely to set. Everything else has a working default.
saas.*
| Value | Published default | Meaning |
|---|---|---|
saas.enabled | true | Pull mode: enroll with the hosted control plane and long-poll for work. |
saas.enrollmentToken | "" | Your token. When set, the chart creates the token Secret. |
saas.enrollmentTokenSecret.name | trustacks-runner-enrollment | Secret holding the token. |
saas.enrollmentTokenSecret.key | enrollment-token | Key inside that Secret. |
env.TRUSTACKS_RUNNER_CONTROL_PLANE_URL | https://api.trustacks.com | Control plane the runner calls. |
observer.*
Optional. When on, the runner watches your ArgoCD Applications read-only and reports deploy status, so TruStacks can tell you whether a merged change came up healthy.
| Value | Default | Meaning |
|---|---|---|
observer.enabled | false | Turn on the read-only ArgoCD watch. |
observer.argocdNamespace | argocd | Namespace where your ArgoCD Application resources live. |
--set observer.enabled=true --set observer.argocdNamespace=argocdmcpSignals.*
Optional, off by default, runner 0.2.14-rc27 or later. When on, the runner
observes your GitHub repos read-only through GitHub’s official MCP server
and reports structured facts (Dependabot alerts: disabled, CI ran green
3h ago) to the Stack screen, and open alerts to
Signals. Hosted (SaaS) mode only: the chart refuses to
render mcpSignals.enabled=true without saas.enabled=true.
| Value | Default | Meaning |
|---|---|---|
mcpSignals.enabled | false | Turn on MCP Signals. A source also needs its own enabled. |
mcpSignals.sweepIntervalSeconds | 21600 (6 hours) | Time between sweeps. The runner enforces a 300-second floor. The first sweep runs when the pod starts. |
mcpSignals.sources.github.enabled | false | Turn on the GitHub source. |
mcpSignals.sources.github.credentialSecret | trustacks-mcp-github | The Secret holding your token. You create it; the chart never does. |
mcpSignals.sources.github.credentialKey | token | Key inside that Secret. |
mcpSignals.sources.github.lockdown | true | GitHub’s lockdown mode: hides public-repo content from users without push access. A best-effort content filter, not a boundary. On, and not relied on. |
mcpSignals.sources.github.extraRepos | [] | Repos to observe beyond the ones you connected in TruStacks, for example ["acme/orders-api"]. Inventory only: they show on Stack but never become Signals findings. |
The token. A fine-grained personal access token, resource owner the user
or organization that owns the repos, Only select repositories, and these
repository permissions, all Read-only: Actions, Contents, Metadata, Pull
requests, Issues, Dependabot alerts, Code scanning alerts, Secret scanning
alerts. Nothing else, and no write permission. Create the Secret yourself,
keeping the token out of your shell history and out of process arguments
(printf is a shell builtin; --from-literal would put the token in
kubectl’s arguments, visible to anyone who can run ps):
read -rs GH_TOKEN && \
printf '%s' "$GH_TOKEN" | kubectl -n "$NAMESPACE" create secret generic trustacks-mcp-github \
--from-file=token=/dev/stdin && unset GH_TOKENThen turn the source on:
helm upgrade trustacks-runner oci://ghcr.io/trustacks/charts/runner \
--version "$VERSION" \
-n "$NAMESPACE" --reset-then-reuse-values \
--set mcpSignals.enabled=true \
--set mcpSignals.sources.github.enabled=trueCheck the first sweep:
kubectl -n "$NAMESPACE" logs deploy/trustacks-runner-runner | grep -E 'mcp.sweep_done|mcp.source_refused'How the credential is handled. The Secret is mounted into the runner pod as a read-only file, not an environment variable. The runner reads it when it starts the MCP server and passes it only to that child process’s environment. It is never logged (the server’s stderr is redacted for token shapes and for the literal value), never sent to TruStacks, and never a tool argument.
What runs. The GitHub MCP server binary ships inside the runner image, pinned by digest, so it is in the runner’s SBOM and under its cosign signature; nothing is pulled at run time. Each sweep starts it as a short-lived child process in read-only mode, limited to the read tools on our allowlist. If the server offers any tool that is not marked read-only or not on the allowlist, the runner refuses to start the source. See Integration model for the full safety model.
Egress. The GitHub source needs HTTPS (443) to api.github.com, which
the runner already reaches for your repos. A Kubernetes NetworkPolicy cannot
match a hostname: under default-deny egress, use your CNI’s FQDN policy
(Cilium toFQDNs, Calico DNS policy) or allow the api CIDRs GitHub
publishes at https://api.github.com/meta.
policyBundle.*
Where the runner gets the constitution, and how it verifies it. See Policy in force for what happens at start.
| Value | Published default (0.2.14 and later) | Meaning |
|---|---|---|
policyBundle.bundleRef | ghcr.io/trustacks/policy/constitution:1 | The signed constitution to pull. |
policyBundle.verify | true | Verify the signature before loading. Turning this off loads the bundle unverified and the runner reports it as unverified. |
policyBundle.insecure | false | Plain HTTP to the registry. Local development only. |
policyBundle.keyless.certificateIdentityRegexp | trustacks-policy’s publish.yml only, from 0.2.14-rc26 (rc24–rc25 also accepted the retired trustacks-mvp signer; see Supply-chain verification) | Which workflow may have signed the bundle. |
policyBundle.keyless.certificateOidcIssuer | https://token.actions.githubusercontent.com | OIDC issuer for the signing certificate. |
policyBundle.overlayRef | "" | Overlay bundle. Not supported on hosted yet. |
Runner charts before 0.2.14-rc24 have no keyless block and point
bundleRef at a development registry that does not exist in your cluster.
Those runners fall back to the policy baked into the image. The first
stable release with these defaults is 0.2.14.
Pod settings
| Value | Default |
|---|---|
image.repository, image.tag | ghcr.io/trustacks/runner, the chart’s version |
deployKeyLoader.image | alpine/k8s:1.31.4 (from Docker Hub) |
imagePullSecrets | Empty. Set if you mirror the images to a private registry. |
resources.requests | cpu: 50m, memory: 128Mi |
resources.limits | cpu: 500m, memory: 512Mi |
podSecurityContext | runAsNonRoot: true, runAsUser: 10001, fsGroup: 10001 |
securityContext | No privilege escalation, read-only root filesystem, all capabilities dropped |
nodeSelector, tolerations, affinity | Empty |
What the chart creates
| Resource | Name (release trustacks-runner) |
|---|---|
| Deployment, Service (ClusterIP, port 8000), ServiceAccount | trustacks-runner-runner |
| Secret, when you pass the token | trustacks-runner-enrollment |
| Role and RoleBinding | trustacks-runner-runner-deploy-keys, trustacks-runner-runner-secret-writer |
| ClusterRole and RoleBinding, when the observer is on | trustacks-runner-runner-argocd-observer |
The runner writes two Secrets of its own at runtime. Helm does not manage them:
trustacks-runner-identity· the runner’s keypair and ID, so a replaced pod re-enrolls as the same runner.trustacks-runner-llm· your LLM key, unsealed from what the control plane delivered.
Permissions
All namespace-scoped to the runner’s namespace unless stated.
| Grant | Verbs | Why |
|---|---|---|
| Secrets | get, list | An init container loads git deploy keys labeled trustacks.io/role=git-deploy-key (used by local development) |
| Secrets | get, create, update | The runner writes its identity and LLM Secrets |
applications.argoproj.io in observer.argocdNamespace | get, list, watch | Observer only. Read-only: the runner cannot sync, roll back, or delete. |
No cluster-wide grants. The observer’s ClusterRole is bound with a RoleBinding in your ArgoCD namespace only.
Network
The runner opens no inbound ports to the internet; the ClusterIP Service is reachable only inside your cluster. It needs outbound HTTPS to:
| Destination | Why |
|---|---|
api.trustacks.com | Enrollment, job polling, results |
ghcr.io (and its blob storage) | Runner image, chart, policy bundle |
docker.io | The alpine/k8s image used by the deploy-key init container |
api.anthropic.com or openrouter.ai | Your LLM provider |
github.com, api.github.com | Clone your repos, push the proposal branch, open the PR; with MCP Signals on, the read-only GitHub observations |
Sigstore’s TUF and Rekor endpoints (tuf-repo-cdn.sigstore.dev, rekor.sigstore.dev) | Keyless signature verification of the policy bundle |
There is no mTLS tunnel and nothing to allow inbound.
Upgrade
The Runners page shows the published version and the version your runner is running, and generates the upgrade command:
VERSION=0.0.0 # placeholder: the published version the app shows
helm upgrade trustacks-runner oci://ghcr.io/trustacks/charts/runner \
--version "$VERSION" \
-n "$NAMESPACE" --reset-then-reuse-values--reset-then-reuse-values takes the new chart’s defaults, including the new
runner image, then reapplies the values you set yourself (for example
saas.enrollmentToken if you passed it on the command line, and any observer
settings).
Never upgrade with --reuse-values. It reuses every value from the
previous release, including the old image tag baked into the old chart. The
upgrade reports success and the old runner keeps running. Confirm the running
version on the Runners page after every upgrade.
Uninstall
helm uninstall trustacks-runner -n "$NAMESPACE"
kubectl -n "$NAMESPACE" delete secret trustacks-runner-identity trustacks-runner-llmThe second command removes the Secrets the runner wrote itself, which Helm does not track. Then delete the runner on Runners. Removing the TruStacks GitHub App from your org is separate, in your GitHub settings.
Uninstalling does not revoke the enrollment token. It is reusable and does not expire. Delete your copies of it, and if it may have leaked, revoke it on Runners with Revoke and generate a new token; see Revoke enrollment tokens.
Verify before you install
cosign verify \
--certificate-identity-regexp '^https://github\.com/TruStacks/trustacks-mvp/\.github/workflows/publish-images\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"ghcr.io/trustacks/charts/runner:$VERSION"The same identity signs the runner image. See Supply-chain verification.
Where to go next
- Policy in force · what the runner is enforcing and how to check
- LLM provider · the key the runner uses
- Architecture · what the runner does and what leaves your cluster