# Keep a credential out of the sandbox

Give a sandbox an authenticated API without placing the token in it: the
claim's egress proxy adds the token to requests for the Connection's hosts,
and the sandbox holds only a placeholder.

## Prerequisites

- A SandboxClass that declares a Connection role, such as `vcs`, in
  `spec.connections`. See [Binding a Connection to a sandbox](/reference/core/connection/#binding-a-connection-to-a-sandbox).
- A ToolRegistry sandbox or an AgentRuntime that binds that role.
- The API authenticates with a header, such as `Authorization: Bearer`, or with
  HTTP basic auth. A request-signing scheme such as AWS SigV4 cannot be
  injected; place that credential with `env` or `files` instead.
- The tools in the sandbox speak HTTP/1.1 to the API. A client that accepts
  only HTTP/2, such as a gRPC client, fails against an injected host.

## Steps

1. Create the Secret in the namespace the sandboxes run in:

   ```bash
   kubectl create secret generic acme-github-token -n agents \
     --from-literal=user=x-access-token \
     --from-literal=token=<your token>
   ```

2. Create the Connection. Each `inject` entry names the hosts it covers and the
   header it sets, written as a template over the Secret's keys:

   ```yaml
   apiVersion: omnia.altairalabs.ai/v1alpha1
   kind: Connection
   metadata:
     name: github-acme
     namespace: agents
   spec:
     hosts: [api.github.com, github.com]
     credential:
       kind: secret
       secretRef:
         name: acme-github-token
     placement:
       inject:
         - hosts: [api.github.com]
           header: Authorization
           value: "Bearer {{ .token }}"
           placeholderEnv: [GH_TOKEN]
         - hosts: [github.com]
           basic:
             usernameKey: user
             passwordKey: token
   ```

   `placeholderEnv` sets `GH_TOKEN` in the sandbox to `omnia-injected-github-acme`,
   so `gh` starts. The proxy replaces whatever `Authorization` the sandbox
   sends. The `github.com` entry covers `git clone` over HTTPS.

3. Optional: limit what the token can be used for. Add `rules` to an entry,
   and the proxy forwards only the methods and path prefixes you list:

   ```yaml
         - hosts: [api.github.com]
           header: Authorization
           value: "Bearer {{ .token }}"
           placeholderEnv: [GH_TOKEN]
           rules:
             - methods: [GET, HEAD]
               pathPrefixes: [/repos/acme/widgets]
             - methods: [POST]
               pathPrefixes: [/repos/acme/widgets/pulls]
   ```

   A prefix matches whole path segments, so `/repos/acme/widgets` does not
   match `/repos/acme/widgets-old`.

4. Bind the Connection to the class's role, on the ToolRegistry sandbox or on
   the agent:

   ```yaml
   # AgentRuntime
   spec:
     sandbox:
       enabled: true
       connections:
         - {sandbox: workspace, role: vcs, connection: github-acme}
   ```

## Verify

1. Start a session that uses the sandbox, then find its claim:

   ```bash
   kubectl get sandboxclaims -n agents
   ```

2. Check the claim's injection objects exist:

   ```bash
   kubectl get secret <claim>-egress-ca -n agents
   kubectl get configmap <claim>-egress-trust -n agents
   ```

3. Check the sandbox holds the placeholder, not the token:

   ```bash
   kubectl exec -n agents deploy/<claim>-sandbox -- printenv GH_TOKEN
   ```

   The output is `omnia-injected-github-acme`.

4. From inside the sandbox, call an allowed path, then one no rule allows:

   ```bash
   kubectl exec -n agents deploy/<claim>-sandbox -- \
     curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/acme/widgets
   kubectl exec -n agents deploy/<claim>-sandbox -- \
     curl -s -D - -o /dev/null https://api.github.com/user | grep -i x-omnia-egress-refused
   ```

   The first prints `200`. The second prints
   `X-Omnia-Egress-Refused: l7-not-allowed`.

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| The claim fails with `ConnectionSecretKeyMissing` or `ConnectionTemplateInvalid` | A key the `value` or `basic` names is not in the Secret | Add the key, or correct the template |
| The claim fails with `ConnectionInjectionInvalid` | The `value` renders to more than one line: a key it reads holds a line break other than a trailing one | Store the value on one line |
| `X-Omnia-Egress-Refused: credential-unavailable` (502) | The proxy cannot read a key the entry needs, as happens between a key's removal from the Secret and the operator dropping the binding | Restore the key. Once the binding is dropped, the host is refused with `host-not-allowed` |
| `X-Omnia-Egress-Refused: host-mismatch` (403) | The request's `Host` header names a different host from the one the tool connected to | Send each host's requests on its own connection |
| A Python or git command fails certificate verification for an injected host | `REQUESTS_CA_BUNDLE` or `GIT_SSL_CAINFO` is not set: the operator found no root certificates to build `bundle.pem` from | Check the operator image has a root bundle, such as `/etc/ssl/certs/ca-certificates.crt` |
| A Python or git command fails certificate verification for another host | That host's root CA is baked into the sandbox image only, and `bundle.pem` holds the operator image's roots | Reach that host from a claim that binds no injecting Connection |
| A long-running process fails TLS to an injected host after several days | The claim's CA was renewed and the process loaded its roots before that | Restart the process |

## Related pages

- [Connection: injected credentials](/reference/core/connection/#injected-credentials):
  every `inject` field and what the proxy does on an injected host
- [Credential injection](/explanation/security/credential-injection/): how it
  works and what it costs