Run agents in a sandbox
Run each user's agent in a Kubernetes sandbox that does not hold credentials. The Keycard egress proxy allows only the destinations you list and adds a credential for the user to each request.
This guide runs an agent in a Kubernetes pod that does not hold credentials and reaches only the destinations you list. The egress proxy runs beside the agent, allows or denies each outbound connection, and gets a credential from your Zone for each request that needs one. Each user gets their own sandbox, and the Zone issues credentials for that user only; Isolate an agent’s network access explains why.
The steps use the reference sandbox, which runs on agent-sandbox. Treat it as a working example to adapt, since the Zone configuration is the same for any deployment.
How it works
Section titled “How it works”flowchart LR
A[Agent] -->|"captured by iptables"| P[Egress proxy sidecar]
P -->|"ServiceAccount token, user ID"| K[Keycard Zone]
K -->|"credential for the user"| P
P -->|"request with credential"| R[Resource]
- An init container installs iptables rules in the pod that send the agent’s outbound TCP and DNS to the proxy and drop everything else.
- The proxy allows a request only when an endpoint in its configuration covers the host, port, and path.
- For an endpoint with a credential, the proxy runs an impersonation token exchange, authenticating with the pod’s projected ServiceAccount token and naming the user.
- The Zone checks the token against your cluster’s issuer, evaluates policy, and returns a credential for that user and Resource, which the proxy writes into the request.
Prerequisites
Section titled “Prerequisites”- A Kubernetes cluster that supports native sidecar containers, which are on by default from Kubernetes 1.29 (Sidecar Containers).
- A ServiceAccount token issuer the Zone can fetch over HTTPS, such as the OIDC issuer of an EKS or AKS cluster; the Zone reads
<issuer>/.well-known/openid-configurationand the keys it names. - Permission to create a namespace at the
privilegedPod Security level, because the init container needsNET_ADMINandNET_RAW. - A Keycard Zone with a User for each person whose agent will run, and a Provider for each upstream, such as GitHub.
- Each of those Users has authorized the brokered Resources once, so Keycard holds their grant. Act on Behalf of Absent Users shows a landing page for this step.
- An organization service account with access to the Zone, which the setup tool authenticates with and the sandbox never uses.
- A clone of
keycardai/agent-sandbox-examples, and mise to install its pinned tools withmise install.
Configure the Zone
Section titled “Configure the Zone”zone-setup in the reference repository creates every Zone object the proxy needs, from one values file. You can run it again safely, because it finds what exists and changes only what differs. If you manage the Zone another way, What zone-setup creates lists each object.
-
Write the values file
Copy
zone-setup.example.yamltozone-setup.yamland fill it in:management_api_url: https://api.keycard.aizone_url: <keycard-issuer-url>management_service_account:client_id: <service-account-client-id># Copy it from the cluster exactly, including any trailing slash.issuer_url: <cluster-oidc-issuer-url>namespace: agentsusers:- email: troy.barnes@greendale.edu- email: abed.nadir@greendale.eduresources:- identifier: https://api.github.comname: GitHubprefix: truecredential_provider: <github-provider-identifier>- identifier: https://github.comname: GitHub gitprefix: truecredential_provider: <github-provider-identifier>Each Resource
identifiermust equal the canonical URL of a proxy endpoint. The proxy sends that URL as the exchange’sresource, and the Zone matches it exactly, so writehttps://api.github.com, nothttps://api.github.com/.For an upstream that takes a static API key, set
secret_envto the name of an environment variable that holds the key, in place ofcredential_provider.zone-setupstores the key in the Zone, and the proxy gets it for each request without the agent seeing it. -
Run zone-setup
Terminal window export KEYCARD_MANAGEMENT_CLIENT_SECRET=<service-account-client-secret>go run ./cmd/zone-setup --values zone-setup.yaml > zone-setup.out.yamlAdd
--dry-runfirst to print what it would change.zone-setupstops if another Policy Set is active in the Zone, and because--replace-active-policy-setreplaces that set for everyone in the Zone, read the active set before you use the flag.zone-setup.out.yamlholds the IDs the next section uses:k8s_sandboxfor the Zone, and onesandbox_user_accessentry per user.
What zone-setup creates
Section titled “What zone-setup creates”| Object | Setting |
|---|---|
| Provider | Issuer URL set to the cluster’s issuer, without client credentials; the Zone discovers the signing keys. |
| Application | Identifier urn:keycard:sandbox-proxy:<namespace>, consent set to implicit, because the user is not present to answer a consent screen. |
| Resources | One per resources entry, with its credential Provider or stored secret. |
| Dependencies | Each Resource is a dependency of the Application, so a new dependency applies to every user’s sandbox. |
| Workload Identity credentials | One per user on the Application, with the cluster’s issuer as its Provider and system:serviceaccount:<namespace>:<user-id> as its subject. Its ID is the proxy’s client_id. |
| Policies | Two per user, in a Policy Set named Kubernetes Sandbox, which zone-setup activates. |
The ServiceAccount for each user is named after the user’s Keycard ID, and the two Policies tie that ServiceAccount to that user. The first lets the Application impersonate the user, for its dependencies only, and only with that user’s ServiceAccount token from your cluster:
permit ( principal is Keycard::Application, action, resource) when { principal.identifier == "urn:keycard:sandbox-proxy:agents" && context has impersonate && context.impersonate == true && context has subject && context.subject.identifier == "<user-identifier>" && context has actor_claims && context.actor_claims has issuer_claims && context.actor_claims.issuer_claims.contains({"key": "iss", "value": "<cluster-oidc-issuer-url>"}) && context.actor_claims.issuer_claims.contains({"key": "sub", "value": "system:serviceaccount:agents:<user-id>"}) && principal.dependencies.contains(resource)};The second lets the user reach Resources through that same ServiceAccount:
permit ( principal is Keycard::User, action, resource) when { principal.identifier == "<user-identifier>" && context has actor_claims && context.actor_claims has issuer_claims && context.actor_claims.issuer_claims.contains({"key": "iss", "value": "<cluster-oidc-issuer-url>"}) && context.actor_claims.issuer_claims.contains({"key": "sub", "value": "system:serviceaccount:agents:<user-id>"})};Both must permit the exchange. When you remove a user from the values file and run zone-setup again, it removes their Policies from the set and archives them, so their sandbox stops getting credentials. Policy Schema lists the attributes these conditions read.
Install the sandbox
Section titled “Install the sandbox”-
Pin the images
The egress proxy publishes three images to
ghcr.io/keycardai:egress-netsetup,egress-proxy, andsandbox-base. Each release attachesdigests.txtwith the digest of each image; name images by digest, because tags move. Build your agent’s imageFROMsandbox-base, which installs the proxy’s CA before the agent starts. -
Install the cluster chart
Create
install-values.yaml:zoneURL: <keycard-issuer-url>images:netsetup: ghcr.io/keycardai/egress-netsetup@sha256:<digest>proxy: ghcr.io/keycardai/egress-proxy@sha256:<digest>agent: <your-agent-image>@sha256:<digest>rbac:subjects:- kind: Groupname: sandbox-adminsapiGroup: rbac.authorization.k8s.ioThen install it:
Terminal window ./install/install.sh --context <kube-context> --values install-values.yamlThe script installs the agent-sandbox controller and the chart, which creates the
agentsnamespace at theprivilegedPod Security level, the sandbox pod template, asandbox-adminRole bound torbac.subjects, and a NetworkPolicy that denies ingress and allows egress only to the cluster DNS and to TCP 443 on public addresses. -
List the endpoints
Copy
sandbox/values.example.yamltosandbox-values.yaml. Set the Zone URL, your cluster’s pod and service ranges, and the endpoints the agent may reach:zoneURL: <keycard-issuer-url>policy:clusterCIDRs:- <pod-cidr>- <service-cidr>endpoints:- url: https://api.github.comcredential:placement: {kind: header, name: Authorization, template: "Bearer {credential}"}- url: https://github.comcredential:placement: {kind: basic, user: x-access-token}- url: https://pypi.org- url: https://files.pythonhosted.orgAn endpoint with
credentialgets a credential from the Zone; for an endpoint without one, the proxy allows the request and does not call the Zone. Thebasicplacement letsgit cloneandgit pushwork over HTTPS without a credential helper. Egress proxy lists every placement. -
Create a sandbox for each user
Pass the user’s entry from
zone-setup.out.yaml:Terminal window go run ./cmd/new-sandbox --values sandbox-values.yaml \--namespace agents \--user-id <user-id> --user-email troy.barnes@greendale.edu \--service-account system:serviceaccount:agents:<user-id> \--client-id <client-id> \--provider-id <provider-id> --application-id <application-id> \--application-identifier urn:keycard:sandbox-proxy:agentsnew-sandboxcreates the user’s ServiceAccount, the proxy’s configuration as a ConfigMap, and the Sandbox, and only the proxy container mounts the ConfigMap and the ServiceAccount token. Add--dry-runto print the manifests without applying them.
Verify
Section titled “Verify”Find the sandbox’s pod with kubectl -n agents get pods, then run three requests from its agent container:
kubectl -n agents exec <sandbox-pod> -c agent -- curl -sS -o /dev/null -w '%{http_code}\n' https://pypi.org/simple/kubectl -n agents exec <sandbox-pod> -c agent -- curl -sS https://api.github.com/userkubectl -n agents exec <sandbox-pod> -c agent -- curl -sS https://www.google.com/- The first prints
200; the endpoint does not have a credential, so the proxy does not call the Zone. - The second returns the GitHub account of the sandbox’s user, even though the agent did not send a token.
- The third fails with “Could not resolve host”, because the proxy’s resolver answers
NXDOMAINfor any name that is not an endpoint host.
Read the proxy’s decisions:
kubectl -n agents logs <sandbox-pod> -c egress-proxy | grep '"decision"'Each line is one decision with the sandbox ID, the destination, the verdict, and the reason. Then open the egress proxy Application in Keycard Console and select its Activity tab, which shows one credentials:issue for the GitHub request and the sandbox’s user; the other two requests do not appear there.
Troubleshooting
Section titled “Troubleshooting”The proxy names the reason for each refusal in the X-Keycard-Deny-Reason response header and in its audit record.
A credentialed request returns 403 exchange_denied
The Zone refused the exchange on policy. Check that:
- The user’s two Policies are in the active Policy Set.
- The Resource is a dependency of the Application.
- The sandbox runs as the ServiceAccount named after its user. A sandbox configured for one user and running as another user’s ServiceAccount always gets this answer.
A credentialed request returns 502 exchange_failed
The exchange did not complete, and the record’s decision.error holds the Zone’s description.
invalid_clientmeans the Zone did not match the token to a Workload Identity credential. Compare the Provider’s Issuer URL with the token’sisscharacter for character, and check that the Zone can fetch the issuer’s discovery document.insufficient_authorizationmeans the user has not authorized that Resource yet.
The proxy never becomes ready
Read its log with kubectl -n agents logs <sandbox-pod> -c egress-proxy. Before it serves traffic, the proxy checks that the init container’s receipt matches its own ports and user ID, that it does not hold NET_ADMIN, and that net.ipv4.ip_unprivileged_port_start is above its ports. If a check fails, the proxy does not start, and its log names the failed check, usually with the setting to change. The egress proxy’s security model lists each check.
What’s next
Section titled “What’s next”- Egress proxy lists the configuration document, the audit record, and every reason code
- Isolate an agent’s network access explains what this deployment guarantees, and what you take on
- Authenticate deployed workloads sets up Workload Identity credentials on other platforms
- Audit Log & Sessions shows the Zone’s record of each credential it issues