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.
Member-set environment variables that start with
LD_ are refused.
Gates and services
Gates run in a separategate 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.- The runner’s
gitandghhelpers ask the dashboard for a token, proving themselves with the attempt’s own credentials. - The dashboard mints a short-lived installation token narrowed to the task’s repository and a fixed permission set.
- The helper caches it in memory only (
emptyDirwithmedium: Memory) and renews it before expiry. - At the end of the attempt the token is revoked.
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 setmcp.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 orplainchallenge 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:readandfactory: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.254and 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, default9464) answers only withAuthorization: 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.seriesin 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.