# How credentials are protected

> A vendor credential is entered once in the console, decrypted in one component, and never enters a sandbox, a tool's code or a model. What holds that line, step by step.

Source: https://docs.getgraft.ai/graft/security/how-credentials-are-protected

A connection is one vendor account you have given Graft: its auth scheme, its credential and the set of hosts it may reach. The credential is what everything else is arranged around. It is entered in one place, stored encrypted, decrypted in one component, and never present where model-written code runs. This page walks the path a request takes and names what refuses at each step.

## Entry

A secret is typed only into the console, on the connection form, whether you opened it from an agent's handoff or from **Connections → Add connection**. The agent's `request_connection` proposes the vendor, the hosts, the scheme and the non-secret parameters, and takes no secret; the form shows the proposal, lets you edit it, names every host the credential will be sent to, and has the secret inputs. A tool that finds its credential rejected asks for re-entry with `request_credential`, which likewise returns a link and takes nothing. There is no path by which a secret reaches Graft through the chat, and the Hermes skill tells the agent never to ask for one there.

After entry the credential is write-only. The console shows the scheme, the hosts and when the credential was set, never the value, and the row a request path reads carries no credential material.

## Storage

The credential is encrypted before it is stored, bound to your account and to that one connection, so ciphertext for one connection cannot be read back as another's. In self-hosted Graft the keyring's key is derived from `GRAFT_KEYRING_SECRET`; in Graft Cloud the keyring is KMS-backed. The connection service holds only the encrypt half of the keyring. Decryption happens in one component, the proxy, at the moment a request is about to leave for the vendor.

## In the sandbox

Authored code runs in a sandbox: a short-lived Docker container in self-hosted Graft, a Blaxel sandbox in Graft Cloud. Its only egress is the proxy; the internal network it lives on has no gateway and no external DNS. The module is handed a frozen context with four things, `ctx.fetch`, `ctx.proxyBase`, `ctx.proxyKey` and `ctx.connection`, and none of them is a credential.

`ctx.proxyKey` is a **capability token**: an EdDSA-signed JWT minted for this one run, naming you, the agent, the connections in reach, the tool, and whether this is a dry run. It lives for the run's timeout plus one minute, about two minutes for an ordinary run, and the runner removes every `GRAFT_*` variable from the module's environment before the module loads. What the token buys is one connection's credential, injected by the proxy, for those minutes. Model-written code can print it; that is the accepted risk, and it is bounded to that connection for that long.

## At the proxy

Every request from a sandbox arrives at the proxy on a path naming the connection. The proxy then, in this order:

- **Verifies the token** statelessly against the deployment's key pair, and refuses one that is missing, expired or invalid.
- **Checks the connection is yours and is named in the token.** A connection outside the agent's scope was never in the token, so the request is refused before anything else is read.
- **Resolves the target** against the connection's primary host, or against one of its declared hosts when the SDK form `/h/<host>/` names one, and refuses a host outside the set. The URL is built so that nothing in the caller's path can move the host.
- **Refuses private addresses**, twice: the hostname literal is judged before the request, and the address it resolves to is judged inside the resolver the socket opens through. Private, loopback, link-local and cloud-metadata ranges are refused in both IPv4 and IPv6, as are internal names such as `localhost`, `.internal` and `.local`.
- **Strips every credential-shaped inbound header**, `authorization`, `proxy-authorization`, `x-api-key`, `x-graft-token` and `cookie`, then sweeps the token by value from any other header or query parameter an SDK may have put it in. Trace and edge headers that would describe Graft's own infrastructure are dropped too.
- **Stops a write in a dry run** here, before any credential is decrypted, answering a 202 preview with header names only.
- **Decrypts the credential and applies the scheme**: an API key header or query parameter, bearer, basic, an OAuth2 client-credentials exchange, an authorization-code token refreshed when spent, Unleashed's HMAC signature, or a Snowflake key-pair JWT. The scheme is Graft's code. The agent chooses a scheme and its parameters and never supplies signing code.
- **Refuses to follow redirects.** A vendor 3xx goes back to the caller with anything the scheme put on the URL scrubbed from its `Location`. `GRAFT_PROXY_FOLLOW_REDIRECTS` is the break glass, and even under it a hop stays inside the connection's host set, on a public address, at most three deep.
- **Redacts an echoed credential on the way back.** A vendor that quotes the key it refused, or echoes the request it was sent, would otherwise hand the plaintext to the sandbox. The proxy replaces every occurrence of each injected value in response headers and text-like bodies with `[redacted:credential]` and marks the response `x-graft-redacted: credential`. Binary bodies pass through untouched.

Request bodies are capped at 10 MB and the whole call at 30 seconds. Every call, whatever happened, is one wide event carrying the connection, the agent, the tool and the outcome, and never the token or the credential.

## A connection Pipedream holds

On Graft Cloud, a vendor Pipedream Connect covers, Gmail first, is connected with one click and the token never reaches Graft. The ladder above runs unchanged up to the point where a credential would be decrypted: the token is verified, the connection checked against it, the target resolved and pinned to the vendor's hosts, private addresses refused, inbound credentials stripped, a dry run's write stopped. Then, instead of decrypting anything, the proxy rewrites the request into Pipedream's proxy path with the vendor URL encoded in it, names the account and your Pipedream user id, authenticates as Graft, and carries your headers under Pipedream's `x-pd-proxy-` prefix with the ones Pipedream refuses dropped. Pipedream adds the vendor's token and calls the vendor; the vendor's answer comes back through the same ladder, redirects judged against the vendor's hosts and Graft's own Pipedream token redacted if it is ever echoed. What Graft stores for such a connection is the account's id at Pipedream and nothing else; revoking it asks Pipedream to delete the account link.

## Before it runs at all

The static check refuses a module before it can be published if it names a literal foreign host in `ctx.fetch` (`fetch-absolute-url`), calls bare `fetch` (`global-fetch`), constructs an SDK with a credential or base of its own (`sdk-not-bound`), reads the exec's environment (`execute-environment`), imports `child_process`, `net` or `dgram`, imports outside its own directory, or imports a package its manifest does not declare. Packages install at publish, under the package policy, and never in a sandbox.

## Withdrawing

**Revoke** a connection under **Connections** and every agent loses it at once; a tool bound to that vendor stays published and asks again after reconnection. **Revoke token** on an agent's page and that harness is refused at the MCP endpoint immediately; one leak costs one agent. **Re-enter credential** replaces a rotated key and touches no approval. The agent token itself is stored as a hash with a visible prefix, so the console cannot show it again either.

Handoff links are signed with `GRAFT_HANDOFF_SECRET`, bound to the action and the agent that asked, expire, and open once. A tampered, expired or used link gets a refusal page and approves nothing.

## Where next

- [What the model can and cannot do](https://docs.getgraft.ai/graft/security/what-the-model-can-and-cannot-do.md): The two models in the loop, and the line between them and your credentials.
- [The proxy and sandboxes](https://docs.getgraft.ai/graft/concepts/the-proxy-and-sandboxes.md): The concept page for the one route out.
