DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones Build AI Agents That Are Ready for Production
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
Build AI Agents That Are Ready for Production

Just dropped: New 2026 “Cloud-Native Foundations” Trend Report. See how teams are tackling complexity, cost & reliability.

AI can investigate. Engineers still decide. See how both work together across incident response in this DZone + Datadog webinar on Oct. 29.

Related

  • Building Threat Intelligence Pipelines Using Python, APIs, and Elasticsearch
  • The Architect's Guide to Logging
  • Building a Production-Ready MCP Server in Python
  • Online Developer Tools a Backdoor to Security Threat

Trending

  • Pipelines on Fire: Why Your CI/CD Tools Are the New Cyber Battlefield
  • Dashboards and Queries for Apache Kafka
  • Common Pitfalls in RAG Applications: What to Avoid When Using Vector Search and Embeddings
  • Agentic Systems and Design Patterns
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway

Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway

Deploy agentgateway as a security proxy between Goose AI agents and Quarkus MCP servers to enforce JWT auth, RBAC, and tool-poisoning guardrails.

By 
Daniel Oh user avatar
Daniel Oh
DZone Core CORE ·
Sep. 09, 26 · Analysis
Likes (2)
Comment
Save
Tweet
Share
3.7K Views

Join the DZone community and get the full member experience.

Join For Free

In Part 1 of this series, we built a Quarkus-based MCP tool server and connected it to the Goose AI agent over Streamable HTTP. The tools worked, the demo was clean, and everything ran on localhost. But the moment you imagine 50 developers running Goose on their laptops, all hitting the same set of backend MCP servers, the architecture starts to crack. Who authenticated that tool call? Which role authorized the getAuditTrail invocation? What stops a poisoned tool name from injecting payloads into your backend?

This article answers those questions by placing agentgateway — the Linux Foundation's open-source proxy for agentic AI traffic — between Goose clients and the Quarkus MCP microservices we built in Part 1.

The Problem: Direct Agent-to-Backend Connections Don't Scale

When Goose (or any MCP client) connects directly to a backend MCP server, every tool call is a point-to-point trust relationship:

This works for demos. It breaks in production for three reasons:

  1. No authentication. The MCP Streamable HTTP endpoint accepts any JSON-RPC call. There is no token verification, no session binding, and no identity propagation.
  2. No authorization. Every caller can invoke every tool. An intern running Goose has the same access as an SRE — getAuditTrail, getOrderStatus, everything.
  3. No guardrails. A compromised or misconfigured agent can send tool names containing prototype-pollution payloads (__proto__), path-traversal sequences (../), or CRLF-injected headers. The backend has to defend itself alone.

The Solution: agentgateway as a Unified Control Plane

agentgateway is a Rust-based proxy purpose-built for AI agent traffic. It understands the MCP protocol natively — it doesn't just forward HTTP; it parses JSON-RPC envelopes, manages MCP sessions, and applies policies at the tool-call level. Here is the architecture we're building:

Goose connects to agentgateway on port 3000. agentgateway validates the JWT, checks the caller's roles against tool-level RBAC rules, passes the call through an ExtMCP guardrail server that sanitizes headers and blocks poisoning attempts, and only then forwards the clean request to the Quarkus backend on port 8080.

Prerequisites

You'll need everything from Part 1, plus:

agentgateway binary (v1.4+):

Shell
 
curl -sL https://agentgateway.dev/install | bash


Verify your Part 1 Quarkus MCP server still works:

Shell
 
cd part1-quarkus-mcp
mvn quarkus:dev


Then confirm the MCP endpoint responds:

Shell
 
curl -s http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' | jq .


Step 1: Deploy agentgateway Alongside the Quarkus MCP Server

Create the agentgateway configuration at part2-agentgateway/agentgateway/config-dev.yaml. This development config proxies MCP traffic without requiring JWT, so you can validate the plumbing first:

YAML
 
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
mcp:
  port: 3000
  policies:
    cors:
      allowOrigins:
        - "*"
      allowHeaders:
        - mcp-protocol-version
        - content-type
        - mcp-session-id
      exposeHeaders:
        - Mcp-Session-Id

  targets:
    - name: customer-tools
      mcp:
        host: http://localhost:8080/mcp


Start agentgateway:

YAML
 
agentgateway -f part2-agentgateway/agentgateway/config-dev.yaml


Now test the proxied MCP endpoint. Note that agentgateway returns SSE format (event: message\ndata: {...}), so we extract the JSON from the data: line:

Shell
 
curl -s http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-03-26" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  | grep '^data: ' | sed 's/^data: //' | jq .


You should see the same customer-tools server info as Part 1, but the traffic now flows through agentgateway. Open http://localhost:15000/ui to see the agentgateway admin UI with your MCP target listed.

Step 2: Add JWT Authentication

With the proxy working, let's lock it down. The mcpAuthentication policy implements the MCP Authorization specification — it validates JWT bearer tokens on every MCP request and supports OAuth 2.1 with PKCE for browser-based flows.

Update the config to part2-agentgateway/agentgateway/config.yaml:

YAML
 
# yaml-language-server: $schema=https://agentgateway.dev/schema/config
mcp:
  port: 3000
  policies:
    cors:
      allowOrigins:
        - "*"
      allowHeaders:
        - mcp-protocol-version
        - content-type
        - mcp-session-id
        - authorization
      exposeHeaders:
        - Mcp-Session-Id

    mcpAuthentication:
      issuer: http://localhost:9000
      audiences:
        - "http://localhost:3000/mcp"
      jwks:
        url: http://localhost:9000/.well-known/jwks.json
      resourceMetadata:
        resource: http://localhost:3000/mcp
        scopesSupported:
          - "mcp:tools:read"
          - "mcp:tools:execute"
        bearerMethodsSupported:
          - header

  targets:
    - name: customer-tools
      mcp:
        host: http://localhost:8080/mcp


How It Works

When a Goose client (or any MCP client) connects to http://localhost:3000/mcp:

  1. Discovery. The client fetches /.well-known/oauth-protected-resource from agentgateway and discovers it needs a bearer token with the mcp:tools:execute scope.
  2. Token acquisition. The client runs the OAuth 2.1 Authorization Code flow with PKCE against the issuer (http://localhost:9000), obtains an access token, and includes it as Authorization: Bearer on subsequent MCP requests.
  3. Validation. agentgateway downloads the JWKS from the issuer, verifies the token signature, checks exp, iss, and aud claims, and extracts the sub and role claims for downstream authorization.
  4. Forwarding. Only after validation does agentgateway forward the JSON-RPC call to the Quarkus backend.

Connecting to a Real OIDC Provider

For production, replace the issuer and JWKS URL with your OIDC provider. Here is an example using Keycloak:

YAML
 
    mcpAuthentication:
      issuer: https://keycloak.example.com/realms/mcp
      audiences:
        - "https://gateway.example.com/mcp"
      jwks:
        url: https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs
      provider:
        keycloak: {}


agentgateway has built-in support for Keycloak, Auth0, Okta, Microsoft Entra ID, and other OIDC providers.

Step 3: Configure Tool-Level RBAC With CEL Expressions

JWT authentication tells you who is calling. MCP authorization tells you what they're allowed to do. agentgateway uses CEL (Common Expression Language) to define fine-grained, tool-level RBAC rules.

Add the mcpAuthorization policy to your config:

YAML
 
    mcpAuthorization:
      rules:
        # Operators can call any tool
        - 'has(jwt.roles) && "operator" in jwt.roles'
        # Viewers can only read status and health
        - >
          has(jwt.roles) && "viewer" in jwt.roles &&
          mcp.tool.name in ["getCustomerStatus", "getZoneHealthLogs", "getSLACompliance"]
        # Auditors can access audit trail and SLA compliance
        - >
          has(jwt.roles) && "auditor" in jwt.roles &&
          mcp.tool.name in ["getAuditTrail", "getSLACompliance"]


How the Rules Work

Each rule is a CEL expression that evaluates to true (allow) or false (deny). agentgateway evaluates them in order — the first match wins.

Role Allowed Tools Denied Tools
operator All five tools None
viewer getCustomerStatus, getZoneHealthLogs, getSLACompliance getOrderStatus, getAuditTrail
auditor getAuditTrail, getSLACompliance getCustomerStatus, getZoneHealthLogs, getOrderStatus
No role None All


These aren't abstract labels — at Acme FinServ they map to real people and a real segregation-of-duties story:

Persona Role Why this scope
Sofia — SRE, on-call for the platform operator Needs to drive operational tools during incidents; full access is justified and logged.
Acme Status Dashboard — an internal read-only service viewer Shows customers and health at a glance; must never read getOrderStatus or getAuditTrail (PII/financial).
Priya — external SOC 2 auditor auditor Reviews the audit trail and SLA posture only. Giving her getCustomerStatus would violate least privilege — an auditor reading live customer data is itself a finding.


The auditor scope is the one a SOC 2 assessor will scrutinize: it proves the audit function is separated from the operational function, and that access is granted by need, not convenience.

agentgateway also auto-filters tools/list responses — if a viewer calls tools/list, they only see the three tools they're authorized to invoke. The agent never even learns that getAuditTrail exists.

Available CEL Variables

Variable Description
mcp.tool.name The tool being invoked (e.g., getCustomerStatus)
mcp.tool.target The backend target name (e.g., customer-tools)
jwt.sub The subject claim from the JWT
jwt.roles Role claims extracted from the JWT
has(jwt.) Check whether a JWT claim exists


Step 4: Prevent Tool Poisoning With ExtMCP Guardrails

JWT and RBAC protect the identity layer. Guardrails protect the content layer. A valid, authenticated operator can still send a tool call with a poisoned name like getCustomerStatus/../../../etc/passwd or arguments containing

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook