Security model

What AgentBox protects, how, and, just as important, what it doesn't. The full threat model is THREAT_MODEL.md in the repository; this page follows it.

Status

Pilot-ready for a platform team that reads this page. AgentBox has not had an external security review, and the control plane is still partial. docs/security-review-scope.md in the repository scopes that review.

Protected assets

  • GitHub App private key and installation tokens
  • Model API provider keys and entitlement source credentials (Jira, ServiceNow, Okta, Entra, GitHub org)
  • Repository write authority delegated to an agent
  • The delegation bound: an agent never acting with more authority than its issuing developer
  • Runtime identity, issuer keys, and policy configuration
  • Approval decisions and audit history

The delegation bound

The security claim is that an agent's authority is at most the developer's, and usually less, and that the agent is always distinguishable from the developer.

How it holds today

  • Entitlements are keyed to the human, not the agent: the directory export, Okta, and Entra by human email, GitHub teams by ghLogin, Jira and ServiceNow elevation only when the ticket is assigned to human.
  • Default deny narrows from there: allow rules with requires need every requirement satisfied; an outage makes them unknown, which can't satisfy an allow but still lets deny and approval apply.
  • Separation is structural: the assertion's jti and runtime ID are in every decision and approval; the issuing human can't approve that runtime's requests; revocation never touches a human credential.

Where it's weaker than the claim

  • Checked at issuance and, when enabled, at verification. The issuing service refuses a team the developer doesn't hold and always sets human to the authenticated login. With identity.delegation: "enforce" the broker also refuses an assertion whose team isn't among the teams or groups its own sources report for human, failing closed when a source is down. Without it (the default), an assertion minted by the script, or by an issuer with no directory configured, carries whatever its operator typed. application and mode aren't checked against sources in either place. An allow rule written without requires opts out of the bound.
  • The issuer is trusted for the developer's identity. "Developer X delegated to runtime Y" rests on the identity provider's login via oauth2-proxy and on the issuer's Ed25519 key, which lives on the issuer host as a file (AWS KMS doesn't offer Ed25519; an HSM-backed issuer key is future work). Whoever can run the script with that key can mint any identity. Issuer administrators see and can revoke every runtime.
  • Revocation propagates within the poll interval. The issuer writes revocations.json; agentd re-reads it when it changes (every 5 seconds by default), on SIGHUP, or takes signed bundles from the control plane. A revoked assertion is accepted for up to one interval. A malformed rewrite keeps the previous list in force.
  • Directory data can be stale. Static exports, the 5-minute entitlement cache, and cached Okta and Entra lookups mean offboarding can take effect late. Revoke the runtime explicitly when a developer's access changes. The Okta and Entra credentials are read-only directory scope; a compromised broker can enumerate group membership, nothing more.
  • Egress and model traffic are attributed to the runtime, but not bound to it. Given the issuer keys, the egress proxy (assertion as proxy credentials) and the model gateway (assertion as a request header) verify the assertion a workspace sends and keep quotas and budgets per runtime, but neither can check the token binding, so a workspace that obtains another runtime's assertion can spend that runtime's egress quota and model budget under its name (never its Git authority). Workspaces that send no assertion are attributed by source address. The delegation bound does not extend to what those services allow, only to the broker's own decisions.

Trust boundaries

Untrusted: the agent, its commands, repository contents, package scripts, prompts, and issue text.

Trusted: the broker process, host operator, configuration, state directory, private key, and container/network administrator.

In static mode the daemon serves one runtime identity to every client presenting the per-broker bearer token, so deploy one broker per isolated workspace. In fleet mode the daemon holds no workspace token: a request is accepted only when its bearer token hashes to the binding inside a valid, unrevoked assertion, so the issuer decides which token is which runtime. A workspace can still read its own token and assertion; a stolen pair works until expiry or revocation, and one workspace's pair never authenticates as another. Never publish the listener to a shared or public network.

IDE attachment

  • The editor's server is workspace code: it runs as node with the same reach as the agent, and so does every extension it installs. It cannot exceed the workspace's authority, which the broker, proxy, and Compose service define.
  • Editor credential injection is disabled, and direct GitHub is denied by the proxy anyway.
  • The IDE's own model traffic (Cursor, Copilot, Windsurf) goes from your machine to the vendor, outside the gateway and the audit log. Treat the IDE vendor as a trusted party for prompt confidentiality, as your existing IDE agreement already does.
  • Allowlisting IDE hosts widens egress to marketplaces, which are third-party code sources.
  • Hosted agents (Cursor background agents, Copilot coding agent, Codex cloud) never touch the workspace or the broker.
  • Laptop mode (broker and agent as the same OS user) demonstrates policy and audit; it enforces nothing against a hostile agent.

Hosted agents

The hosted-agents service governs vendor-hosted agents through GitHub itself, so its guarantees are GitHub's. It trusts GitHub's webhook signature and delivery (a leaked secret forges at worst a pending approval or a failing check, never a success without policy). It trusts the ruleset: the check is decorative unless every protected branch requires it and no GitHub App is a bypass actor, which startup and hourly verification fail closed on. The human is best-effort, derived from the pull request and recorded with humanSource; a run whose human is unknown can only be approved by someone else. A compromised process can post check conclusions and create hosted approvals, nothing more. A reviewer's decision reaches the check within one poll interval (default 10 s); until then the pull request is unmergeable.

GitHub App signing key

With GITHUB_PRIVATE_KEY_PATH the RSA key is read by agentd alone from a file-backed secret. With AGENTGATE_KMS_KEY_ID the key never exists on the broker host: agentd sends each signing input to AWS KMS and receives only the signature. A compromised broker can sign while it runs (it holds kms:Sign) but cannot exfiltrate the key, and revoking the role's grant ends its ability instantly. Credentials and KMS responses are never logged. The signer refuses to start with both a key file and a KMS key configured.

Implemented controls

  1. Credential isolation. The key and tokens live in agentd. Authentication is added only to fixed GitHub HTTPS endpoints; redirects are rejected; client authorization headers aren't forwarded.
  2. Least-privilege tokens. Minted for one numeric repository ID with only the permission the operation needs.
  3. Default deny. Unknown resources and unmatched actions are denied; deny overrides approval and allow.
  4. Parse before forwarding. All pushed refs are checked before any receive-pack body is sent. Malformed packets fail closed.
  5. Bound approvals. Grants bind repository, refs, object IDs, runtime, and config digest; expire in 15 minutes; are consumed once, durably.
  6. Separate admin channel. Approvals use an owner-only Unix socket, not the workspace listener. oidc: reviews require an HMAC signature; self-approval and duplicate reviewers are rejected.
  7. Audit-or-nothing. Audit append failure prevents forwarding. State files are owner-only; one daemon per state directory.
  8. Bounded resources. Request sizes, concurrency, headers, and upstream waits are limited. Rate limiting per address and per runtime runs before authentication, and rejection floods are summarized rather than written per request.
  9. Network isolation. The Compose workspace is internal-only with closed DNS, dropped capabilities, and no broker state.
  10. Projected APIs. PR/CI access is an allowlist of methods and paths; response fields are projected explicitly and upstream error bodies aren't relayed.
  11. Bound PR approvals and human-only merges. Merges are never allowed without approval; the broker re-reads the PR immediately before merging and aborts if the base or head moved.

Known limits

  • A compromised host, broker, Docker daemon, or same-UID process can bypass controls. Fleet workspaces share the proxy and gateway client tokens (attribution is by assertion, not by credential), and a flood from one address still throttles every workspace behind it at the broker.
  • Allowed writes can exfiltrate data. A permitted push or PR can carry workspace data. Scanning is regex-based and misses encoded or split secrets; commit messages and LFS objects aren't scanned.
  • Reads are repository-level. There's no branch-level read filtering.
  • GitLab's OAuth token is not narrowed per request. It carries the bot user's full scope on every project it can reach; default-deny policy and the bot's project membership are the boundary.
  • Reviewers approve a SHA, not a diff. They must inspect the commit on GitHub.
  • The approval web UI is in the trust boundary. Its admin secret can approve as any oidc: reviewer.
  • The issuer key is trusted. Whoever holds it can mint any runtime identity.
  • Protected-branch drift only warns. Rules are enforced at startup; the hourly recheck logs a warning.
  • Allowlisted upload hosts and TLS content to allowlisted hosts are remaining egress channels.
  • Unicode homoglyphs across scripts aren't detected when allowUnicodeRefs is on; reviewers see escaped code points.
  • A compromised model gateway holds the provider keys; prompts pass through it in clear text on the internal network.

Supply chain

  • Zero npm runtime dependencies.
  • Release images carry an SPDX SBOM, SLSA provenance, and keyless cosign signatures.
  • Each image digest is Trivy-scanned (failing on fixable HIGH/CRITICAL) before it's tagged.
  • CI runs checks, tests, a protocol fuzzer, the demo, the Docker smoke test, and the fleet smoke test.

Reporting

Please report vulnerabilities privately through GitHub security advisories rather than public issues.