πŸ›‘οΈ Reference Architecture Β· KubeCon

MCP Toolbox as a
Security Control Plane

An AI agent shops across PostgreSQL and MongoDB through one gateway β€” and cannot read another user's data, see a credential, or run raw SQL. The agent lives outside the trust boundary; every guarantee is enforced by MCP Toolbox and the database.

01

The trust boundary

agent β†’ Toolbox β†’ databases
⚠ Untrusted zone
πŸ–₯️ Web UI + Agent
FastAPI Β· Google ADK Β· Kimi-K2.6 (Nebius)
🧠 LLM reasoning
Decides which tool to call. Sees only typed tool signatures β€” no identity, no SQL, no creds.
β€” keycloak_token (JWT) β†’
Trust boundary
πŸ›‘ Control plane
βš™οΈ MCP Toolbox
Validates the token, binds identity, runs curated tools as a least-priv role.
#1 custom tools, no execute_sql
#2 authenticated params (OIDC claim)
#3 authRequired (admin gate)
#5 parameterized SQL ($1…$n)
#9 OpenTelemetry on every call
πŸ—„ Data (system of record)
🐘 PostgreSQL
users Β· products Β· carts Β· orders. Role toolbox_app (least-priv).
πŸƒ MongoDB
$vectorSearch over Gemini embeddings. Role toolbox_ro.
πŸ”‘ Keycloak (OIDC)
Issues the JWT. Prod swap β†’ Google Sign-In.
Untrusted β€” the model & its arguments Trust boundary β€” token validated here Toolbox β€” the only thing that touches data Databases β€” least-privilege roles
02

Request lifecycle

"show my orders"

Browser β†’ agent-web

The signed-in user's JWT is attached server-side. The user never handles raw tokens.

Agent picks a tool

The LLM calls list_my_orders(). Its schema has no username β€” it cannot ask "as whom".

Toolbox validates & binds

Verifies JWT signature, issuer & audience against the OIDC provider; extracts preferred_username; binds it into the SQL parameter. The token header is keycloak_token.

Parameterized SQL as a least-priv role

Runs … WHERE u.username = $1 as toolbox_app. A forged body value is ignored β€” the claim wins. No token β†’ 401.

Audited result

Rows return; an OpenTelemetry span records the tool, source, duration & outcome. Another user's data is simply unreachable.

03

Ten security mechanisms

all verified Β· scripts/verify_security.sh β†’ 11/11
1

Custom tools, not execute_sql

The agent gets 8 purpose-built tools, never arbitrary query power. Vector index/path are hardcoded.

toolbox/tools.yaml
2

Authenticated parameters

verified

username is bound from the OIDC claim & stripped from the model's schema. It can't be supplied or forged.

authServices:[{name:keycloak, field:preferred_username}]
3

Authorized invocations

verified

Admin tools need a grocery-admin audience only carol holds (role-driven in Keycloak).

authRequired:[keycloak_admin]
4

Bound parameters (SDK)

store_region is fixed by the app, absent from the model signature. (App-layer trust β€” the confused-deputy line.)

agent/agent.py Β· bind_param()
5

Parameterized queries

verified

Every statement uses $1…$n; tool args are values, never SQL. Injection β†’ treated as a literal.

toolbox/tools.yaml statements
6

Least-privilege roles

verified

toolbox_app can't UPDATE the catalog, DELETE orders, or DROP. One role per source.

db/postgres/01_roles.sql
7

IAM / Workload Identity

On GKE, Cloud SQL via IAM β€” no DB password exists in the cluster.

deploy/k8s Β· cloud-sql-postgres (no password)
8

Secrets handling

Credentials live only in Toolbox via ${ENV} β€” never reach the model; Secret Manager in prod.

.env (local) Β· Secret/CSI (GKE)
9

Observability

verified

Every tool call is a traced, audited event. A blocked read is a logged 0-row call, not a silent drop.

--telemetry-otlp β†’ OTel Β· Jaeger
10

Network hardening

Only agent→Toolbox→DB. The agent can't reach the database directly.

--allowed-hosts Β· k8s NetworkPolicy
04

What the model can & can't set

the deterministic guarantee

πŸ” What Toolbox enforces

view_cart(username πŸ”’)
add_to_cart(username πŸ”’, sku, store_region πŸ”’, quantity)
checkout(username πŸ”’)
get_order_details(order_id, username πŸ”’)
πŸ”’ = bound by Toolbox from the verified token / app β€” invisible to the LLM.

πŸ€– What the model sees

view_cart()
add_to_cart(quantity, sku)
checkout()
get_order_details(order_id)
No username, no store_region. The agent cannot pass what isn't there.
05

Defense in depth

five layers, the agent passes none on trust
🧰Curated tool surfaceNo raw SQL; only 8 named, typed tools exist.
πŸͺͺAuthenticated parametersIdentity bound from the token β€” unforgeable, server-side.
🚦Authorized invocationsAdmin tools gated by a role-driven audience.
πŸ”’Parameterized queriesArguments are values, never executable SQL.
πŸ›‘οΈLeast-privilege DB roleEven a subverted tool can't DROP / mutate the catalog / delete orders.
06

Local demo vs. production

same tools, stronger primitives
ConcernLocal (Docker Compose)Production (GKE)
Postgres authkind: postgres + passwordcloud-sql-postgres Β· IAM, no password
Identity to DBrole password in .envWorkload Identity KSA β†’ GSA
tools.yamlbind-mounted filemounted from a Secret
Secrets.envSecret Manager (CSI / ESO)
Networkcompose bridgeNetworkPolicy: agent β†’ Toolbox β†’ DB
Telemetry--telemetry-otlp β†’ collector--telemetry-gcp β†’ Cloud Trace
Identity providerlocal KeycloakGoogle Sign-In
Scalingsingle containerHPA 2–10, pooled connections
07

Stack

βš™οΈ
MCP Toolbox 1.4.0
security control plane
🧠
Kimi-K2.6 Β· Nebius
agent LLM (pluggable β†’ Gemini)
πŸ”—
Google ADK + LiteLLM
agent + tool-calling
πŸ”’
Gemini embeddings
3072-d vector search
🐘
PostgreSQL 16
transactional system of record
πŸƒ
MongoDB Atlas Local
$vectorSearch
πŸ”‘
Keycloak 26
OIDC identity
πŸ“Š
OTel + Jaeger
traces & metrics
☸️
Docker Compose Β· GKE
local + production