Skip to main content
This page is for a security reviewer. It states what each control does and what it does not. Names of values refer to the Kubernetes chart. See Kubernetes and Helm to set them.

The short version

  • An agent runs code you did not write, next to credentials. Bellows confines it with hardened containers, separate user ids, a network policy, and an admission policy. It holds no long-lived credential.
  • The agent gets a short-lived GitHub token for one repository, fetched on demand. The token is not in its environment.
  • This is not kernel or VM isolation. Every runner shares the node’s kernel with other pods.
  • Membership is not a sandbox. Anyone in an organization can queue a command that an agent runs on any tracked repository.

Trust boundaries

Keep the runner node group separate from the dashboard’s nodes (runner.nodeSelector, runner.tolerations). Then agent code never shares a node with the process that holds the App key.

Runner isolation

Hardened containers

On Kubernetes, the admission policy requires these on every pod the driver creates. The cluster refuses a pod that does not meet them. One exception exists. The agent container starts as root with only SETUID and SETGID added, so it can start commands as another user. A declared service or a gate image has no such exception. If a service needs root at boot, it declares a numeric user: "uid:gid" that is not 0. Pod Security restricted cannot admit the agent container. baseline can.

Separate users

Commands the agent runs execute as the tool user. The CLI’s configuration, credentials and mailboxes stay closed to that user. The control channel to the supervisor is reached only through a proxy in the CLI’s user. No tool environment holds a control token.
The tool user protects the CLI against accidents, not against a hostile tool. A tool can still write files the CLI later reads (.git/config, .claude, .mcp.json) and can exhaust memory, CPU or process slots. Hooks and MCP servers run as the CLI’s user. Treat what an agent can do inside its container as unconstrained.
Member-set environment variables that start with LD_ are refused.

Gates and services

Gates run in a separate gate sidecar, and declared services run as svc-<name> sidecars. They share the task’s workspace and reach each other on the pod’s loopback. Only these sidecars and the reviewer may restart. Admission refuses anything else.

What the agent can reach

Declared services resolve by name to 127.0.0.1 inside the pod, not through DNS.

GitHub access and the credential broker

The agent never holds a standing GitHub credential.
  1. The runner’s git and gh helpers ask the dashboard for a token, proving themselves with the attempt’s own credentials.
  2. The dashboard mints a short-lived installation token narrowed to the task’s repository and a fixed permission set.
  3. The helper caches it in memory only (emptyDir with medium: Memory) and renews it before expiry.
  4. At the end of the attempt the token is revoked.
The token is not in the runner’s environment, not in a Secret, and not in git config. Connection credentials such as Jira use the same broker. Publishing a pull request is a supervised step: the supervisor runs it in its own container with its own copy of the token, over an authenticated channel. The supervisor’s three per-attempt tokens each open a different door, and any other route answers 403. The GitHub App’s private key is the worst secret to leak. It mints installation tokens indefinitely, and the only fix is a new key. It reaches the dashboard only. Set chmod 600 on key files, and keep the runner node group apart from the dashboard.

Secrets handling

A secret a task can read is a secret the agent can use, and a prompt could ask it to print it. Give tasks only the secrets they need, and scope them to a repository.

Authentication

The chart has no open mode. An unauthenticated POST /api/jobs would be remote code execution, so the Ingress is off by default and you enable it on purpose. Sign-in uses the OAuth App with the read:org scope and reads no repository data. Access follows installation membership, re-checked on every request. A removed membership ends sessions and tokens on the next request. With the webhook configured (see Create the GitHub App), a GitHub-side removal takes effect as soon as GitHub reports it. Without it, it waits for that person’s next sign-in. The first member of an organization is its admin. Demoting the last admin is refused.

Tokens

Only hashes of tokens are stored. Each credential kind works on its own routes and never falls through to another. A worker token is refused on human routes, and the reverse.

MCP scope

MCP is off unless you set mcp.enabled. It requires an https public URL, since the endpoint carries bearer tokens. See Connect an MCP client.
  • The authorization server accepts public clients only, with PKCE S256. A missing or plain challenge is refused.
  • The user approves each client on a consent screen that only a signed-in browser session can confirm. The screen cannot be framed (frame-ancestors 'none', X-Frame-Options: DENY).
  • Two scopes exist: factory:read and factory:write. A refresh can only narrow them.
  • Some routes are closed to every scoped token: minting or revoking credentials, writing secrets or connections, authorizing a provider, and managing members, notification destinations and executor profiles. An MCP grant cannot be turned into a longer-lived credential or a wider reach.
  • Members see and end their own grants in the connected-apps list. Tokens die when the membership does.
  • Client registration is open and rate limited by caller address in each process only. Behind a proxy the address is the proxy’s.

Network

  • Terminate TLS at your Ingress. Set auth.cookieSecure: 'true'.
  • Runner egress is controlled by a NetworkPolicy. It works only if your CNI enforces it. Check from inside a runner pod that traffic to 169.254.169.254 and to private ranges fails.
  • IPv6 egress is not covered. Every rule is IPv4.
  • A NetworkPolicy does not restrict HTTP routes. A runner can call any public URL.
  • The driver’s metrics port (driver.metricsPort, default 9464) answers only with Authorization: Bearer <JOB_BOARD_TOKEN>. It has no Service. The driver refuses a metrics port with no token off loopback.
  • The OTLP collector accepts unauthenticated telemetry unless you set telemetry.ingestToken.
  • On EKS, also set the node’s metadata hop limit to 1 so a pod cannot reach node credentials. See Kubernetes and Helm.

Uploads

Attachment type is decided from the file’s bytes, not its declared type. SVG, HTML and macro documents are refused. Downloads are sandboxed attachments, and only three image types may display inline. A draft upload is readable by its uploader only. Text extraction runs in the runner with no board credentials, under a timeout and an output cap. Artifact store credentials live in the dashboard only.

Known limits

  • Not kernel or VM isolation. For a hard boundary, run runner nodes under gVisor, Kata Containers or Firecracker. The chart does not provision them.
  • The network controls do nothing without an enforcing CNI.
  • A taken-over run’s gate requests keep the lost driver’s endpoint, and a node that goes NotReady is noticed only when the cluster acts on it (about 300 seconds by default).
  • A task volume is a thread boundary. A volume’s size is a request, and a cap only where the provisioner enforces it.
  • attrs.series in telemetry is an unsalted one-way digest, so a guess can confirm a value.
  • Membership gives access to every tracked repository. GitHub repository permissions are not mirrored.