Are you an LLM? You can read better optimized documentation at /docs/agent-guard/network/credentials-for-hosts.md for this page in Markdown format
Credentials for allowed hosts
An allowed host is reachable with whatever the agent sends it. To call an API that needs a token, the agent needs the token, and a token passed with --pass-env or a need sits in the agent's environment: every process the agent starts can read it, send it to any host on the list, or paste it into the model's context.
An agent definition can give the credential to the Jozu gateway instead. A spec.egress entry with headers names one host and the headers the gateway sets on every request to it. The agent's tools reach that host authenticated, and the value never enters the microVM's environment, files or tool output. A registry entry, oci://ghcr.io, does the same for a container registry with the login your desktop already holds.
This page explains how both work, walks through a GitHub token, and states what the mechanism does not cover. For entry syntax and record fields, see the network access reference.
How it works
A header entry is an object in spec.egress:
yaml
spec:
needs:
- name: GITHUB_TOKEN
egress:
- host: api.github.com
headers:
Authorization: "Bearer ${needs.GITHUB_TOKEN}"The entry puts api.github.com on the allow list, as the plain name would, and changes how the Jozu gateway handles connections to it:
- Agent Guard resolves
GITHUB_TOKENon your desktop before the microVM boots, as it resolves any need. - The agent's environment receives
GITHUB_TOKEN=agentguard-withheld-GITHUB_TOKEN. Clients such asghsend nothing when their token variable is unset, so the agent gets a value to send, and the gateway replaces it. - Inside the microVM,
api.github.comresolves to the Jozu gateway. The gateway terminates TLS with a certificate from the run's certificate authority, sets each declared header on every request, replacing any value the client sent under that name, and forwards the request over its own TLS connection to the addressapi.github.comresolved to on your desktop before boot. It verifies the host's certificate against the system trust roots. Headers the entry does not declare pass through unchanged. - The gateway asks Agent Guard on your desktop for the value the first time a request needs it, over the microVM's host channel, and keeps it in memory for the run. Every answer is written to the session's credential audit file on your desktop.
A host with headers is handled differently from every other listed host:
| Listed host | Host with headers | |
|---|---|---|
| TLS | From the agent's client to the host. The gateway reads only the name | Ends at the gateway, which opens its own connection to the host |
| What the gateway sees | The name the client asked for | Every request and response |
| Upstream certificate | Checked by the agent's client | Checked by the gateway against the system trust roots |
| Credential | Whatever the agent sends | The declared headers, in place of the agent's |
| HTTP version | Whatever the client and host agree | HTTP/1.1 |
The agent's tools trust the run's certificate authority through the variables the microVM sets for every run: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS and GIT_SSL_CAINFO, so curl, Python, Node.js, Go programs and git connect without configuration. These are the same certificate authority and variables the model API path uses. A client that ignores all of them, or pins the host's certificate, fails on a host with headers.
What the gateway refuses on a host with headers
The credential is for one host, so the gateway refuses the requests that could carry it elsewhere:
| Request | Answer | Why |
|---|---|---|
A Host header that names another host | 421 Misdirected Request, and the connection is closed | A shared CDN address serves many tenants. Without the check, a client could connect for api.github.com and ask for another tenant's host, which would receive the token |
An absolute-form target, such as GET https://other.example.com/ | 421 Misdirected Request, and the connection is closed | The target overrides the Host header |
TRACE | 405 Method Not Allowed, and the connection is closed | The host answers TRACE with the request it received, which would carry the token back to the agent |
An upstream certificate that fails verification closes the agent's connection. Each refusal is a closed record in the egress log, with the reason.
The gateway evaluates no policy on these requests. It terminates TLS to set headers, not to inspect content: ToolPolicy and GuardrailPolicy see what they see on any run.
Worked example: a GitHub token for the API
This example gives Claude Code authenticated access to api.github.com. It starts from the definition in Allow a host and needs the gh CLI on your desktop, signed in.
Step 1: See the request fail without a credential
Allow the host for one run and ask the agent to call the API:
bash
agentguard run claude-code --workspace ~/projects/sample-app \
--allow-host api.github.comRun
curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/userand tell me the output.
The agent reports 401. The host is reachable, and the request carries no credential. Exit the agent.
Step 2: Declare the header
Add the need and a header entry to the definition:
yaml
# my-agent/agent-definition.yaml
apiVersion: agentguard.jozu.dev/v1
kind: AgentDefinition
metadata:
name: my-claude-agent
spec:
framework: claude-code
needs:
- name: GITHUB_TOKEN
egress:
- pypi.org
- files.pythonhosted.org
- host: api.github.com
headers:
Authorization: "Bearer ${needs.GITHUB_TOKEN}"
includes:
- name: vm-policies
type: policy
source:
modelkit: jozu.ml/jozu/agentguard-policies:vm-standardStep 3: Provide the value and run
Set the variable on your desktop, pack the definition and run it:
bash
export GITHUB_TOKEN="$(gh auth token)"
kit pack . -t my-claude-agent:v3
agentguard run my-claude-agent:v3 --workspace ~/projects/sample-appBefore the microVM boots, the run reports where the value went:
▸ Resolving agent runtime needs...
GITHUB_TOKEN resolved (envFrom, target=env) GITHUB_TOKEN withheld (egress:api.github.com); the agent receives a placeholderAsk the same question again:
Run
curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/userand tell me the output.
The agent reports 200. Ask it for echo $GITHUB_TOKEN and it prints agentguard-withheld-GITHUB_TOKEN. The GitHub CLI works the same way: after sudo apt-get install gh in the microVM, gh api user --jq .login prints your GitHub login. gh sends the placeholder, and the gateway replaces it.
Step 4: Read the records
The boot record lists the hosts the gateway sets headers on, with header names and need names, never values:
bash
log="$(ls -t ~/Library/Logs/AgentGuard/egress_audit.*.jsonl | head -1)"
jq -c 'select(.kind == "boot") | .injections[]' "$log"json
{"host":"api.github.com","headers":["Authorization"],"needs":["GITHUB_TOKEN"]}Each connection to the host writes an inject record when it closes:
bash
jq -c 'select(.kind == "inject") | {sni, headers, needs, requests}' "$log"json
{"sni":"api.github.com","headers":["Authorization"],"needs":["GITHUB_TOKEN"],"requests":1}The credential audit records each time the gateway was given the value, and for which host:
bash
jq -c '{need, consumer, type, source, outcome}' \
"$(ls -t ~/.agentguard/host-private/credential-audit/credential_audit.*.jsonl | head -1)"json
{"need":"GITHUB_TOKEN","consumer":"egress:api.github.com","type":"secret","source":"GITHUB_TOKEN","outcome":"answered"}What just happened
- With the plain entry, the agent reached
api.github.comcarrying nothing, and GitHub refused it. - The header entry moved the token from the agent to the gateway. The agent holds a placeholder, and every request to
api.github.comleaves the microVM with the real token, whatever the agent's client put inAuthorization. - The token reaches only
api.github.com. The agent cannot send it to another host on the list, because it never had it.
To let git push and clone private repositories over HTTPS the same way, add an entry for github.com. Git over HTTPS takes the token as Basic credentials, and the basic form encodes them:
yaml
- host: github.com
headers:
Authorization:
basic: "x-access-token:${needs.GITHUB_TOKEN}"The gateway renders the template, encodes it in base64 and sends Basic followed by the encoding. The need holds the raw token.
Where the value comes from
A need's value is resolved on your desktop before the microVM boots.
| Need declaration | Value |
|---|---|
- name: GITHUB_TOKEN | The host variable GITHUB_TOKEN. If it is unset or empty, the keychain item with service agentguard and account GITHUB_TOKEN |
from: {envFrom: GH_PAT} | The host variable GH_PAT. If it is unset or empty, the keychain item with service agentguard and account named after the need, not the variable |
from: {keychain: {service: ..., account: ...}} | The keychain generic password with that service and account. Both are required |
from: {text: ...} | The literal. It is part of the definition, readable by anyone who can pull it |
The keychain keeps the token out of your shell's environment and history. Store it once:
bash
security add-generic-password -s agentguard -a GITHUB_TOKEN -wsecurity prompts for the value. Add -U to replace an existing item. The service is agentguard in lower case, separate from the AgentGuard items Agent Guard keeps for agent sign-ins.
A required need that does not resolve stops the run before boot and says how to provide it:
! GITHUB_TOKEN: environment variable "GITHUB_TOKEN" is not set
Set it on your host: export GITHUB_TOKEN=...
or store it in the keychain: security add-generic-password -s agentguard -a GITHUB_TOKEN -wWhen a need marked optional: true does not resolve, the gateway drops each header that references it, and the run says which. An entry left with no headers stays on the list as a plain host.
A header value can also be a literal with no need, such as X-GitHub-Api-Version: "2022-11-28". Literals are part of the definition, so keep secrets in needs.
Registry logins
A registry entry lets the agent pull from and push to a container registry with the login your desktop's docker or podman already holds:
bash
agentguard run claude-code --workspace ~/projects/sample-app \
--allow-host oci://ghcr.ioIn a definition, the entry is the same string: - oci://ghcr.io. The entry takes a registry host and nothing else: no port, because the gateway terminates only port 443, and no repository path, because a login applies to the whole registry.
The gateway terminates the registry's host and sends your login as Authorization: Basic on each request that carries no Authorization of its own. A registry client signs in once at the registry's token service with that login, then presents the bearer token it was issued, and the gateway leaves a request's own Authorization unchanged. podman pull inside the microVM works with no podman login, and the agent has no registry login file to read.
docker.io and index.docker.io mean Docker Hub, whose clients send API requests to registry-1.docker.io. oci://docker.io puts that host on the list and uses the login docker login or podman login stores for Docker Hub.
Hosts the registry names
A registry sends clients to other hosts: a token service in the 401 challenge that asks a client to sign in, and a content host in the redirect that answers a layer download. Neither is known before boot. The gateway sees both in the registry's responses and admits the host for the rest of the run:
| Named by | Admitted as | Login sent |
|---|---|---|
The realm of a Bearer challenge in a 401 | A login host, terminated like the registry host | Yes, on requests with no Authorization |
The Location of a 3xx | A plain host, checked by name like any listed host | No. The redirect URL carries its own authorization |
Only an https URL on port 443 naming a host, not an address, is admitted, and only from a registry's login hosts. A run admits at most 32 hosts. A host the run already allows is left as it is, so a token service you list plainly receives no login. Do not list auth.docker.io next to oci://docker.io.
Each admission is an admit record. After pulling docker.io/library/busybox:latest and ghcr.io/containerd/busybox:latest:
bash
log="$(ls -t ~/Library/Logs/AgentGuard/egress_audit.*.jsonl | head -1)"
jq -c 'select(.kind == "admit") | {host, from, via, as}' "$log"json
{"host":"auth.docker.io","from":"registry-1.docker.io","via":"realm","as":"inject"}
{"host":"production.cloudfront.docker.com","from":"registry-1.docker.io","via":"redirect","as":"splice"}
{"host":"pkg-containers.githubusercontent.com","from":"ghcr.io","via":"redirect","as":"splice"}ghcr.io names itself as its token service, so only its content host is admitted.
Where logins are read from
Agent Guard looks up the login the way docker and podman do:
- Docker's config file,
~/.docker/config.json, orconfig.jsonin$DOCKER_CONFIG, including the credential helper it names, such as Docker Desktop's. - Podman's auth files, when Docker's config holds no login for the registry:
$REGISTRY_AUTH_FILE,$XDG_RUNTIME_DIR/containers/auth.json, then~/.config/containers/auth.json.
docker login ghcr.io or podman login ghcr.io on your desktop stores one. Agent Guard reads the login again every 15 minutes during the run, so a login renewed mid-run reaches the agent's next requests. That matters for Amazon ECR, whose login expires after 12 hours; refresh it with:
bash
aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-2.amazonaws.comA registry with no login on your desktop is reached anonymously, so public images still pull:
! registry login: no login for docker.io on this machine; the agent reaches it anonymously (docker login docker.io, or podman login, stores one)A login stored as an identity token, with no password, cannot be sent as Basic credentials, and the registry is reached anonymously after a warning.
Credentials for MCP servers and models
The same withholding applies to credentials the gateway spends on its own requests.
Remote MCP servers. headers on an mcp module with source.endpoint take ${needs.NAME} values, as egress headers do. The gateway sets them on its requests to the server, and the need is withheld from the agent entirely, with no placeholder, because the agent never calls the server itself. If the agent's own tools also need the token for the same service, add a header entry for the host they call: the agent then receives the placeholder, and both paths stay authenticated.
Models on a custom runtime. An llm module names the need it authenticates with in credential:
yaml
- name: assistant
type: llm
source:
model: openai/gpt-4o-mini
credential: OPENAI_API_KEYcredential is accepted only when spec.framework is a container image. An agent CLI reaches its provider itself, with its own key. When a module declares a credential, the provider's conventional variables, ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and GOOGLE_API_KEY, are kept out of the application's environment even if your desktop sets them.
A module that writes env is read as credential only in the one form described in Declare a runtime in an agent definition; any other env stops the run.
What this does not cover
- Root inside the microVM. The gateway runs as root and holds each value in memory for the run. Code running as root in the microVM can read it, and the agent can choose code that runs as root through a Debian package's install scripts. Withholding keeps the value out of the agent's environment, files and context. It does not keep it from root. The value never touches the microVM's disk, and nothing that produces it, such as your keychain or a credential helper, is in the microVM.
- What the agent sends to the host. The agent can make any request the token permits. A token for
api.github.comwith write scope lets the agent write. Scope the token, and restrict actions with ToolPolicy. - Policy on these requests. The gateway evaluates no policy on requests to a host with headers or a registry.
- HTTP/2 and certificate pinning. The gateway offers only HTTP/1.1. Clients that fall back work; clients that require HTTP/2, such as gRPC, fail, as do clients that pin the host's certificate.
- Wildcards, other ports and addresses. A header entry names one exact host on port 443. SSH is not covered.
- Run-time headers.
--allow-hosttakes registry entries but not headers. Headers are declared in a definition, where they are reviewed and signed. - Address changes during the run. The gateway connects only to the addresses the host resolved to on your desktop before boot. A host that does not resolve there is reported, and its connections are closed.
- Local MCP servers. A local MCP server receives the placeholder, as the agent does. A server that checks its token's format before sending rejects it.
- Open mode. Headers and registry logins apply with
--dangerously-allow-all-hosts, because the host still resolves to the gateway. A client that connects to a hard-coded address bypasses the gateway there and carries only the placeholder.
Next steps
- How the list is enforced and where the gateway sits: How network access works
- Entry forms and refused entries: Network access reference
- The
injectandadmitrecords: Logs and audit - Values that do reach the agent's environment: Pass credentials
