Security & production¶
A generated agent authenticates every request, calls only the APIs you allow, can make a person approve risky calls, and runs in a locked-down pod. This page explains each control, what it does not cover, and the checklist to finish before production traffic.
The security model¶
Authentication on every surface¶
One auth policy (shared-bearer, jwt or custom) guards /chat, the thread and approval
routes, the A2A card and JSON-RPC, and the LangGraph Server API under langgraph-server. Only
the probes, /metrics (unless METRICS_TOKEN is set) and the dev-only pages are outside it.
An unknown or misconfigured policy fails closed at startup.
Threads and A2A tasks belong to the principal that created them (a person also reaches
what their agents did for them). Roles in
AUTH_READ_ACROSS_ROLES may read other principals' threads, never continue or delete them.
An agent calling for a user (a delegated request) is refused until AUTH_ALLOWED_ACTORS lists
it, reaches only the work it started for that user, holds none of the user's roles and never
decides an approval. See Authentication.
Outbound calls are allow-listed¶
api-policy.yaml lists every API a tool may call and the methods and operations it may use.
There is no default access: a call the policy does not allow is refused before it is sent, and
lint checks the same rules statically in CI. Widening access is a reviewed change
(api-policy.yaml is in CODEOWNERS), and optional per-API limits cap the calls per run and per
minute. See Outbound API policy.
The policy governs calls made through the policy client (get_client()). A tool that opens its
own HTTP connection bypasses it
(KI-005):
review tool code, and restrict egress with a NetworkPolicy.
Human approval of writes¶
An API's approval block makes chosen calls wait until the requester, or another principal
holding a role, approves exactly that call. It is sent once as approved, or never. It is the
control for write actions that a planted instruction could trigger, and it is a choice per
API, never on by default. See Human approval.
Tool results are untrusted input¶
The API policy decides which endpoints a tool may call, not on whose behalf. Text a tool returns, such as a customer's order note, a ticket comment or an upstream error, reaches the model beside the user's request. Instructions planted there can make a privileged user's agent act on another customer's record, or copy data where someone else can read it: prompt injection turning the agent into a confused deputy.
The template reduces this risk in layers:
| Layer | What it does |
|---|---|
| The fence | UntrustedToolResults (in app_, wired into agent.py) wraps every tool result the model reads as untrusted data, and, when another agent asks for the user, that agent's request too, with a note saying who wrote it. |
| The prompt | The default system prompt says tool output is data, never instructions. |
| Tool checks | Write tools call require_ (the id must appear in the user's own message; when another agent asks, in the user's own words it forwarded too) and, under a per-user policy, require_owner (the record belongs to the caller). require_ keeps a tool for requests the user makes directly. |
| Per-user upstream authorization | Write-capable APIs use auth: forward, or auth: exchange for another agent (a token minted for that agent alone, in the user's name), so the upstream authorizes each user itself. |
| Approval gates | A person sees each concrete write before it is sent. |
| Eval cases | Cases with planted instructions, where expect. asserts the planted write never reached a gate. |
These lower the risk; they do not remove it (see Limitations).
Secrets¶
Secrets stay in the allow-listed Kubernetes Secret: never in values files, workflow logs,
command lines or printed output. Only Principal.public_attributes() is persisted, logged or
traced, and principal ids are hashed in logs and traces. See Secrets.
Data egress¶
Tracing is off by default, and TRACE_CAPTURE=metadata keeps prompts, completions and tool data
out of traces when it is on. A hosted model provider receives the prompts, tool results and
context the agent assembles: decide what may leave your network before you connect one. The
offline profile keeps everything on your network.
Deploys¶
Outside dev, a deploy needs an explicit or confirmed kube context and its own env file, so
development keys never reach staging or prod. It never rotates the live API_KEY implicitly,
and it only rolls back its own revision. Production's desired state changes only through a
reviewed pull request (argocd) or the production environment gate (helm-push). See
Deploy to Kubernetes and CI/CD.
Supply chain¶
The CLI is published on PyPI and as a pinned git tag of each release; setup and update
install it from the tag, and setup installs the skills from the same tag.
Generated projects pin the CLI in .github/agent.env, install from committed lock files, and
pin the base images, the uv version, the subchart versions and the subchart image digests. CI
and CD jobs disable extension overrides (GRAPH_AGENTS_CLI_DISABLE_OVERRIDES=1).
Pods¶
Pods run as a non-root user with a read-only root filesystem, no capabilities and no service-account token. Probes and metrics stay inside the cluster: the route publishes only the API paths.
Thread ids¶
Thread ids are one namespace shared by every caller. An id another principal used first is
theirs (403 for everyone else), so a predictable id can be claimed ahead of its intended user,
and a 403 reveals that an id is taken
(KI-001).
Let the server generate ids (omit thread_id on the first turn), or generate unguessable ones,
such as UUID4, in the client. Never derive a thread id from user data.
What it does not do for you¶
| Not included | Do it with |
|---|---|
| Inbound rate limiting and per-caller quotas (KI-022) | your Gateway or ingress controller. Outbound calls have per-API limits. |
| Web application firewall rules | your edge or Gateway |
| TLS termination | the Gateway, the Ingress or cert-manager (tls.* in the chart) |
| Network isolation: the NetworkPolicy is off by default (KI-033) | examples/ in the chart, on a CNI that enforces it |
| Backups of the agent's database | your database platform |
Production checklist¶
Work through it for staging first, then prod. Each item links to the page that explains it.
Identity and access
- Pick the auth policy:
jwtagainst your identity provider, or acustompolicy you implemented and tested (then setauth_policy_implemented: true). Useshared-beareronly for trusted callers. Authentication - Set
AUTH_READ_ACROSS_ROLESandAUTH_ADMIN_ROLESdeliberately; both are empty by default. Authentication - If other agents call this one for users, list them in
AUTH_ALLOWED_ACTORS(empty refuses them all) and lend roles throughAUTH_DELEGATED_ROLESonly where a tool needs one. If your identity provider's exchanged tokens carry noactclaim, also list the clients people sign in with inAUTH_JWT_DIRECT_CLIENTS: without it an agent's token reads as the user's own, and the agent can decide the user's approvals (a calling agent built from this template sends such a token only when its API setsexchange.allow_actorless: true). Undershared-bearerany holder ofAPI_KEY, another agent included, decides requester gates. Agents calling agents - Give the identity provider's admin what each agent's client may exchange tokens for
(
graph-agents-cli system delegationsprints it for a system of agents), and check that exchanged tokens name the calling agent inactand live 5 minutes or less. Agents calling agents
Tools and outbound calls
- Declare every outbound API with the access it needs and no more
(
graph-agents-cli api add, thenallow/denyfor its operations), withlimitswhere a runaway loop would hurt.graph-agents-cli api checkpasses, and CODEOWNERS coversapi-policy.yaml. Outbound API policy - Every write tool calls
require_user_mentionedon the ids it acts on (andrequire_ownerunder a per-user policy), comparing principal ids exactly; write-capable APIs useauth: forwardwhere the upstream can authorize the user;agent.pykeepsUntrustedToolResults,AnswerInvalidToolCallsand the prompt's tool-results rule. Develop your agent - Agents that call other agents for users use
auth: exchange:TOKEN_EXCHANGE_URLis https, the client secret is insecrets.keys, the identity provider lets each agent's client exchange only for the audiences it calls and keeps exchanged tokens to 5 minutes or less, and each called agent lists its callers inAUTH_ALLOWED_ACTORS. Setexchange.allow_actorless: trueonly for an API whose agent setsAUTH_JWT_DIRECT_CLIENTS(the identity provider's exchanged tokens carry noact);lintnames every API that does. Authentication - Decide which writes wait for a human (
graph-agents-cli api approval):requesterconfirmation for writes users make on their own records,role:approvers (a second person, underjwtorcustom) for actions one person should not take alone. Paused runs and the approvals table need thepostgrescheckpointer. Human approval
Quality gate
-
eval runpasses on the real model, with cases for your tools, refusals, failure modes and instructions planted in tool data, and thepr_checksgate runs on the real provider (its key secret is set). Evaluation, CI/CD
Configuration and secrets
- Record
environments.<env>.contextfor staging and prod in the manifest, and keep.env.stagingand.env.prodout of git. Deploy to Kubernetes -
secrets apply --env <env>, thensecrets status --env <env>exits 0. Secrets - Replace every
CHANGE-ME(registry, chart image, CODEOWNERS owner, Argo CDrepoURL);infra check --env prodreports no required item missing. Deploy to Kubernetes
Database
- External Postgres for staging and prod, with backups; a least-privileged role that owns
its database;
sslmode=verify-fullin the DSN;max_connectionscovers replicas × (DB_POOL_MAX_SIZE+ 1); no transaction-mode PgBouncer in front. External database
Network
- A NetworkPolicy adapted from
deployment/helm/<name>/examples/networkpolicy.yaml, on a CNI that enforces it. NetworkPolicy - A Gateway or Ingress with TLS; review
route.publicPaths; rate limiting at the gateway. The chart -
APP_URL(or the chart'sappUrl, or a hostname) so the A2A card advertises the public URL. The chart - When only other agents call an agent, remove its
/a2a/<agent>path fromroute.publicPathsand admit the callers' pods with the NetworkPolicy rulessystem applywrites (system checkwarns while the path is public, SC13). Deploy a system of agents
Observability and data
-
METRICS_TOKEN(insecrets.keysand the Secret), or a NetworkPolicy, if anything outside the cluster can reach the pods; Prometheus scraping configured, withmetrics.serviceMonitor.bearerToken.enabled(or the token in your scrape job) whenMETRICS_TOKENis set; alerts on failed runs and/ready. Observability -
PRINCIPAL_HASH_SALTset (and added tosecrets.keys) if principal ids are guessable, such as email addresses, and in every agent that asks other agents: it also keys the conversation ids sent to them, which are guessable without it. Observability - Decide
RETENTION_DAYS,TRACING_ENABLEDandTRACE_CAPTUREwith whoever owns the data; publish a privacy notice for a hosted model provider. Observability
Capacity
- Tune
RUN_TIMEOUT_S,RECURSION_LIMIT,resources,replicaCountor the HPA, and the PodDisruptionBudget for your traffic; run the load test intests/load_test/. Observability
Delivery and supply chain
- The GitHub settings in place (
infra checkreports them); pin the actions in the generated workflows to commit SHAs if your organisation requires it. CI/CD - Mirror the base images and vendor the subcharts if the cluster cannot reach Docker Hub. Offline profile
Limitations¶
| Limitation | What to do |
|---|---|
Prompt injection is reduced, not prevented. The fence, the prompt rule and the tool checks depend on the model and on your tools, and some look-alike tags get past the fence (KI-015). An approval gate is only as good as the person reading the call: a requester gate trusts the user to notice a record they did not ask about. |
Gate the writes an injected instruction could abuse, and keep tool-side checks. |
Outbound limits are per process. rate_per_minute is a token bucket in each replica (N replicas allow N times the rate), and max_ is counted in the process that runs the run. |
Rely on the upstream API's own quota for a global cap. |
| No built-in inbound rate limiting (KI-022). | Rate-limit at the gateway or ingress. |
Principal hashes are unsalted unless PRINCIPAL_ is set (KI-002). |
Set the salt and add it to secrets.keys. |
Ownership checks written by hand in a tool may fold case where require_owner does not (KI-043). |
Use require_owner, or compare principal ids exactly. |
Every parked issue, with its severity and workaround, is on Known issues.
Next steps¶
-
Choose the policy that guards every surface.
-
Allow exactly the calls your tools need.
-
Put a person in front of the writes that matter.
-
The chart's pod security, NetworkPolicy and external database.