AgentOS security works in layers. Each one stops a different class of attack, from unauthenticated requests at the edge to cross-user data access in the database.
| Layer | What it stops | Mechanism |
|---|
| Authentication | Unauthenticated requests | JWT validation at the middleware |
| Authorization | Authenticated but out-of-scope requests | RBAC scopes on every endpoint |
| Request isolation | Concurrent requests reading each other’s data | Fresh, isolated object per request |
| User isolation | Cross-user data leakage on shared endpoints | Opt-in using AuthorizationConfig(user_isolation=True) |
AgentOS give you good defaults at every layer
Network-layer controls (rate limiting, WAF, IP allowlists, mTLS) live at your reverse proxy or API gateway layer.
Authentication
A production AgentOS sits behind JWT-validating middleware. Every request carries a token signed by the control plane; your service verifies it with a public key.
from agno.os import AgentOS
agent_os = AgentOS(
agents=[agent],
db=db,
authorization=True, # reads JWT_VERIFICATION_KEY from env
)
authorization=True drives both layers:
- Authentication (this section): requires a valid JWT on every request.
- Authorization (below): enforces the token’s scopes per endpoint.
A small set of public routes are exempt from the JWT requirement: /, /health, /info, /docs, /redoc, /openapi.json, /docs/oauth2-redirect.
Get a JWT_VERIFICATION_KEY
Toggle JWT authorization
Open os.agno.com → Add OS → Live → paste your URL. Enable JWT authorization when connecting a new AgentOS, or later from the OS Settings page. Copy the public key
Copy the public key for your AgentOS from the modal.
Set the verification key
Set the JWT_VERIFICATION_KEY environment variable to your public key in your .env file or export it directly in your terminal:export JWT_VERIFICATION_KEY="your-public-key"
Or, if you manage keys via a JWKS file, point AgentOS at it instead:export JWT_JWKS_FILE="/path/to/jwks.json"
The control plane keeps the private key. Your service only ever sees the public key. JWTs are signed by the control plane and verified by your service. See Generate a Verification Key for the full walkthrough.
Custom JWT configuration
If you’re issuing JWTs from your own auth provider, pass an AuthorizationConfig:
from agno.os import AgentOS
from agno.os.config import AuthorizationConfig
agent_os = AgentOS(
agents=[agent],
db=db,
authorization=True,
authorization_config=AuthorizationConfig(
verification_keys=["public-key-1", "public-key-2"], # rotation
algorithm="RS256",
verify_audience=True,
audience="my-agent-os", # must match the token's `aud`; defaults to the AgentOS id
user_isolation=True, # see below
),
)
verification_keys is a list, so you can rotate keys without downtime. Old tokens validate against the old key, and new tokens against the new. Drop the old key once tokens have expired.
With verify_audience=True, AgentOS rejects tokens whose aud claim doesn’t match the expected audience. That expected value defaults to the AgentOS id; set audience to override it when your provider mints a different value.
JWT claim names (scopes, sub) are configured on the JWT middleware itself, not on AuthorizationConfig. The defaults (scopes for the scopes claim, sub for the user id claim) match the tokens minted by the control plane.
Authorization (RBAC scopes)
Every JWT carries a scopes claim. Endpoints are gated on scopes.
| Scope | Grants |
|---|
agents:read | List agents and read their config |
agents:<id>:run | Run a specific agent |
agents:*:run | Run any agent (same pattern for teams, workflows) |
agent_os:admin | Full access including session, memory, and trace queries |
The control plane mints each token with the appropriate scopes. Scopes are bundled into roles and assigned to users in the control plane: AgentOS ships three default roles (owner, admin, member), and custom roles are available on Enterprise. See the RBAC scope reference for the full scope list, Default Roles for what each grants, and Custom Roles to compose your own.
Request isolation
Every request gets a fresh copy of the agent, team, or workflow it’s hitting. AgentOS calls deep_copy() on the registered component per request, so mutable state (session-scoped variables, tool execution context, run metadata) never bleeds between concurrent calls.
Heavy resources (the DB connection, the model client, MCP tool handles) are shared by reference; only the mutable per-run state is isolated. You get cheap concurrency without the footgun of two requests racing on the same in-memory agent instance.
This is on by default for every run endpoint. There’s nothing to configure.
User isolation
Per-user data isolation is opt-in. RBAC stays in force without it, but routes operate on the unscoped DB. A caller with agents:my-agent:run could read another user’s sessions if they know the IDs. For multi-tenant deployments, turn it on:
from agno.os.config import AuthorizationConfig
agent_os = AgentOS(
agents=[agent],
db=db,
authorization=True,
authorization_config=AuthorizationConfig(
verification_keys=[public_key],
user_isolation=True, # requires authorization=True; the user_id comes from the JWT sub
),
)
With user_isolation=True, every non-admin caller gets:
| Guarantee | How |
|---|
| No cross-user reads | The JWT sub is threaded as user_id on every user-scoped read (sessions, memory, traces). Callers only see their own rows. |
| No cross-user writes | user_id is coerced on every write, so a non-admin can’t persist a session, memory, or trace attributed to another user. |
| Run ownership | Cancel, resume, and continue routes require session_id and verify the run belongs to the caller’s session. |
| WebSocket reconnect | Reconnecting to a workflow run requires session_id and workflow_id, then verifies the caller owns the run. |
Admin callers (whoever holds the configured admin_scope, defaulting to agent_os:admin) bypass all of the above and see the full unscoped view: service accounts, internal tooling, the control plane.
Per-user isolation requires a database that records user_id (PostgreSQL recommended for production).
Defaults
| Concern | Default behavior |
|---|
| Authentication | authorization=False. Opt in with authorization=True and JWT_VERIFICATION_KEY for production. |
| Request isolation | On. Every run endpoint deep-copies the component. |
| User isolation | Off. Opt in via AuthorizationConfig(user_isolation=True) for multi-tenant. |
See the AuthorizationConfig reference for all configuration options and their defaults.