Part 1 proved the shape two agents share. A diagnosis, though, is only a recommendation until someone acts on it, and acting needs privilege. Here two agents look at the same broken database. One can only diagnose it; the other has the identity to fix it. The difference is not in the agents' code or prompts, it is enforced at the gateway.
Who is allowed to act
A read-only dba-diagnoser and a privileged
sre-remediator both reach one MCP server that stands in for the
orders database. The enterprise agentgateway in front of it validates
each agent's identity token and applies a per-tool authorization policy, so the
same server exposes different tools to each agent. The privileged
db_reset_credentials tool is simply invisible to the diagnoser.
Do you need a mesh for this?
No. The authorization happens at the agentgateway that fronts the MCP server, so
there is no Istio or ambient waypoint here. (kagent's AccessPolicy
can also gate MCP tools, but that path enforces through an ambient waypoint and
needs the mesh — a heavier setup for the same outcome.) This lab keeps it to the
gateway. For the full decision — when to reach for EnterpriseAgentgatewayPolicy
versus AccessPolicy, and when to layer both — see the reference:
which authorization layer, when.
The mechanism: per-tool authorization at the gateway
Two EnterpriseAgentgatewayPolicy resources. The first authenticates
every request against Keycloak. The second attaches to the MCP backend and, for
each tool call, evaluates CEL over the token's groups claim and the
tool name. Rules are OR-ed; a tool a caller matches nothing for is filtered from
tools/list and refused on tools/call.
yamlyaml/agentgateway/mcp-authz-policy.yaml
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: mcp-tool-authz
namespace: mock-db
spec:
targetRefs:
- group: agentgateway.dev
kind: AgentgatewayBackend
name: mock-db-mcp
backend:
mcp:
authorization:
action: Allow
policy:
matchExpressions:
# operators can call every tool (no tool-name restriction)
- 'has(jwt.groups) && "db-operator" in jwt.groups'
# readers get the read-only tools only
- 'has(jwt.groups) && "db-reader" in jwt.groups &&
mcp.tool.name in ["db_status","list_tables","db_query"]'
yamlyaml/agentgateway/jwt-policy.yaml
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: jwt-auth
namespace: agentgateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: db-gateway
traffic:
jwtAuthentication:
mode: Strict
providers:
- issuer: "http://keycloak.keycloak.svc.cluster.local/realms/solo"
jwks:
remote:
jwksPath: "/realms/solo/protocol/openid-connect/certs"
cacheDuration: "5m"
backendRef:
group: ""
kind: Service
name: keycloak
namespace: keycloak
port: 80
yamlyaml/agentgateway/backend-route.yaml (the MCP backend)
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
name: mock-db-mcp
namespace: mock-db
spec:
mcp:
targets:
- name: mock-db
static:
host: mock-db.mock-db.svc.cluster.local
port: 8080
path: /mcp
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: mock-db-route
namespace: mock-db
spec:
parentRefs:
- { name: db-gateway, namespace: agentgateway-system }
rules:
- matches: [{ path: { type: PathPrefix, value: /mcp } }]
backendRefs:
- { group: agentgateway.dev, kind: AgentgatewayBackend, name: mock-db-mcp }
Giving each agent an identity
The identity a policy keys on is a token claim. Each agent injects its own
Keycloak token on every MCP call through its RemoteMCPServer's
headersFrom. Two identities in the realm — agent-diagnoser
in group db-reader, agent-remediator in group
db-operator — mean two tokens, and so two tool sets from one server.
yamlyaml/agents/remotemcpservers.yaml
apiVersion: kagent.dev/v1alpha2
kind: RemoteMCPServer
metadata:
name: db-mcp-reader
namespace: kagent
spec:
description: Mock orders database, reached with a read-only (db-reader) identity.
url: http://db-gateway.agentgateway-system.svc.cluster.local:80/mcp
protocol: STREAMABLE_HTTP
headersFrom:
- name: Authorization
valueFrom: { type: Secret, name: agent-token-reader, key: authorization }
---
apiVersion: kagent.dev/v1alpha2
kind: RemoteMCPServer
metadata:
name: db-mcp-operator
namespace: kagent
spec:
description: Mock orders database, reached with a privileged (db-operator) identity.
url: http://db-gateway.agentgateway-system.svc.cluster.local:80/mcp
protocol: STREAMABLE_HTTP
headersFrom:
- name: Authorization
valueFrom: { type: Secret, name: agent-token-operator, key: authorization }
yamlyaml/agents/dba-diagnoser.yaml + sre-remediator.yaml
# read-only diagnoser — references the db-reader RemoteMCPServer
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata: { name: dba-diagnoser, namespace: kagent }
spec:
type: Declarative
declarative:
modelConfig: default-model-config
systemMessage: |
You are a read-only database specialist for "orders". Inspect it, work out
the root cause, and report root_cause, severity, fix, runbook_url. You do NOT
have permission to change the database; name the privileged action the
operator must take rather than attempting it.
tools:
- type: McpServer
mcpServer: { apiGroup: kagent.dev, kind: RemoteMCPServer, name: db-mcp-reader,
toolNames: [db_status, list_tables, db_query] }
---
# privileged remediator — references the db-operator RemoteMCPServer
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata: { name: sre-remediator, namespace: kagent }
spec:
type: Declarative
declarative:
modelConfig: default-model-config
systemMessage: |
You are the privileged on-call operator for "orders". If it is locked because
the superuser password is not set, FIX it by calling db_reset_credentials with
a strong password, then confirm it is healthy.
tools:
- type: McpServer
mcpServer: { apiGroup: kagent.dev, kind: RemoteMCPServer, name: db-mcp-operator,
toolNames: [db_status, list_tables, db_query, db_reset_credentials] }
The mock database
No real Postgres is deployed. images/mock-db is a small MCP server
that simulates the locked orders database: read tools plus one
privileged write, db_reset_credentials, that unlocks it. It never
checks who is calling — the gateway is the sole authority.
pythonimages/mock-db/server.py (tools)
@mcp.tool()
def db_status() -> dict:
"""Report the orders database health: reachable, whether login works, status."""
...
@mcp.tool()
def list_tables() -> dict: ...
@mcp.tool()
def db_query(table: str, limit: int = 10) -> dict: ...
@mcp.tool()
def db_reset_credentials(new_password: str) -> dict:
"""Set the orders superuser password and unlock the database (privileged)."""
db.locked = False
return {"database": db.name, "status": "healthy",
"message": "superuser password set; database unlocked"}
Build it
Separate enterprise kind cluster. Needs an Anthropic key and the two Solo licenses.
export ANTHROPIC_API_KEY=sk-ant-...
export SOLO_LICENSE_KEY=... # Solo Enterprise for kagent
export AGENTGATEWAY_LICENSE_KEY=... # enterprise agentgateway
./scripts/quick.sh up
See the split
The headline: tools/list through the gateway, once per identity.
./scripts/tools.sh
Captured live from the cluster — same endpoint, two identities:
db-reader · dba-diagnoser
3 tools
"http_status": 200,
"tools": [
"db_status",
"list_tables",
"db_query"
]
db-operator · sre-remediator
4 tools
"http_status": 200,
"tools": [
"db_status",
"list_tables",
"db_query",
"db_reset_credentials" <- operator only
]
The gateway filtered db_reset_credentials out of the reader's list.
kagent sees this too — the controller logs the discovery as
db-mcp-reader toolCount 3 and db-mcp-operator toolCount 4,
so the read-only agent's model is never even offered the privileged tool.
One agent can, the other can't
Fire the privileged tool with each identity, straight at the gateway:
./scripts/prove.sh
outputprove.sh — verified live
Before — db_status (as operator): the orders DB is locked
"login": "FATAL: password authentication failed for user \"orders\" ...",
"status": "degraded"
db-reader (dba-diagnoser) tries db_reset_credentials -> REFUSED
"http_status": 400,
"error": { "code": -32602, "message": "Unknown tool: db_reset_credentials" }
db-operator (sre-remediator) calls db_reset_credentials -> SUCCESS
"status": "healthy",
"message": "superuser password set; database unlocked and accepting logins"
After — db_status (as operator): the orders DB is healthy
"login": "ok", "password_set": true, "status": "healthy"
The reader's identity does not just get a 403 — the tool does not exist for it, so
the call comes back as MCP -32602 Unknown tool. The operator's
identity resolves the same tool and the database recovers. One tool call, two
identities, two outcomes.
End to end
And through the agents themselves:
./scripts/ask.sh dba-diagnoser "the orders database is down - diagnose it"
./scripts/ask.sh sre-remediator "the orders database is down - fix it"
In the verified run the diagnoser inspects the database, reports
the root cause and the fix, and signs off: "I cannot perform this myself as it
requires privileged database administrative access that I don't have" — its
identity has no write tool to call. The remediator inspects the
same database, calls db_reset_credentials, and reports it
"back online and fully operational". Same incident, same prompt shape,
different identity — and only one of them can act.
Why this is enterprise-only
The EnterpriseAgentgatewayPolicy backend.mcp.authorization
(per-tool CEL) and jwtAuthentication have no OSS equivalent, and it
runs on Solo Enterprise for kagent plus enterprise agentgateway. Part 1 is
OSS — the shape — and this is the enterprise layer on top: identity and
authorization.
Extending it
- Real backends. Swap the mock MCP server for a real database or Kubernetes MCP server; the policy is unchanged — the identity still scopes the tools.
- Human in the loop. Put
db_reset_credentialsin the operator'srequireApprovalso the privileged action pauses for sign-off. - End-user identity. Carry the calling user's identity through as an exchanged OBO token (see agentic-a2a-kind) so the policy can key on user and agent.
See also
- Reference: EnterpriseAgentgatewayPolicy vs AccessPolicy — which layer, when (decision diagram)
- Part 1: the agentic contract (OSS)
- agentic-a2a-kind — A2A delegation + OBO identity
- agentic-mcp-rbac-kind — per-user MCP tool RBAC at the gateway
- agentgateway docs: MCP tool access control
Versions
Built and verified on:
v1.4.026.3v2.3.40.4.3