A lightweight, security-first A2A gateway in Go.
Develop with sentinel, deploy with agentgateway. Zero agent code changes.
a2a-sentinel is not trying to replace agentgateway (the Kubernetes-native A2A+MCP data plane). Instead, it fills a different need: developers who want to add A2A security to their agents in 5 minutes, without waiting for Kubernetes setup.
| agentgateway | a2a-sentinel | |
|---|---|---|
| For | Platform/infra teams | Individual devs, small teams |
| Deploy | Kubernetes-native | Single binary, docker compose |
| Scope | Full data plane (A2A+MCP+LLM) | A2A security gateway |
| Config | Extensive YAML/API/CRD | Agent Card = your config |
| Security | Configurable | ON by default |
| First request | ~30 min (K8s setup) | ~5 min (docker compose up) |
| Error messages | Standard codes | Educational (hint + docs_url) |
| Bindings | gRPC + REST + JSON-RPC | JSON-RPC + REST + gRPC (v0.3) |
| Management | K8s tools | MCP server (15 tools, v0.3) |
| Migration | — | Zero-effort (same A2A protocol) |
- Two-layer rate limiting (IP-based pre-auth + user-based post-auth)
- Agent Card change detection (cache poisoning defense)
- Authentication modes (JWT, API Key, passthrough-strict default)
- HTTP + SSE proxy (no httputil.ReverseProxy)
- Structured audit logging (OTel-compatible, configurable sampling)
- Educational error messages (hint + docs_url for every block)
- Aggregated Agent Card (merges skills from all backends)
- Graceful shutdown with SSE stream draining
- Health checks (/healthz, /readyz with configurable readiness modes)
- Agent Card JWS signature verification
- Push Notification SSRF protection
- Replay attack prevention (nonce + timestamp)
- Full MCP server (15 tools, 4 resources, MCP 2025-11-25 Streamable HTTP)
- sentinel migrate command (-> agentgateway)
- Card change approve mode (MCP-based)
- Prometheus-compatible metrics endpoint (/metrics)
- gRPC binding support with JSON-RPC protocol translation
- Config hot-reload (SIGHUP + file watch with debounce)
- Extended Prometheus metrics with histograms (prometheus/client_golang)
- Helm chart for Kubernetes deployment
- ABAC policy engine (IP, user, agent, method, time-based, header rules)
- Internationalization (i18n) — Accept-Language based, Korean + English
- Docker and Docker Compose (or Go 1.22+)
git clone https://github.com/raeseoklee/a2a-sentinel
cd a2a-sentinel
docker compose up -d --buildWait for services to be healthy (check logs with docker compose logs -f).
Open http://localhost:3000 for the interactive demo dashboard.
The setup includes two demo agents:
- echo-agent: Standard synchronous A2A agent
- streaming-agent: SSE streaming agent
# Gateway health
curl http://localhost:8080/healthz
# {"status":"ok","version":"dev"}
# Readiness (all agents healthy)
curl http://localhost:8080/readyz
# {"status":"ready","healthy_agents":2,"total_agents":2}
# Aggregated Agent Card (merged from all backends)
curl http://localhost:8080/.well-known/agent.json | jq .JSON-RPC binding (echo agent):
curl -X POST http://localhost:8080/agents/echo/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"text": "Hello from sentinel!"}],
"messageId": "msg-1"
}
}
}'Server-Sent Events (streaming agent):
curl -N -X POST http://localhost:8080/agents/streaming/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "2",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{"text": "Stream test"}],
"messageId": "msg-2"
}
}
}'Each chunk arrives as a separate SSE event. The gateway drains all outstanding streams on graceful shutdown.
gRPC binding (requires grpcurl):
grpcurl -plaintext -d '{
"message": {
"role": "user",
"parts": [{"text": "Hello via gRPC!"}],
"messageId": "msg-3"
}
}' localhost:8443 a2a.v1.A2AService/SendMessageThe gRPC binding translates A2A protocol messages to/from JSON-RPC internally. Agents do not need gRPC support -- sentinel handles the translation.
┌───────────────────────────────────────────────────────────┐
│ a2a-sentinel Gateway │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Security │→ │ Policy │→ │ Protocol │→ │ Router │ │
│ │ Layer │ │ Engine │ │ Detector │ │ │ │
│ │ (2-tier) │ │ (ABAC) │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Audit │ │ Agent Card │ │ Proxy │ │
│ │ Logger │ │ Manager │ │ HTTP/SSE/gRPC│ │
│ │ (OTel) │ │ (polling+agg)│ │ │ │
│ └─────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ┌────────────────────┐ ┌───────────────────────────┐ │
│ │ gRPC Server (:8443)│ │ Config Hot-Reload │ │
│ │ A2A gRPC binding │ │ SIGHUP + fsnotify watch │ │
│ │ ↔ JSON-RPC transl. │ │ Debounce + atomic swap │ │
│ └────────────────────┘ └───────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MCP Server (127.0.0.1:8081) — MCP 2025-11-25 │ │
│ │ 15 tools (9 read + 6 write), 4 resources │ │
│ │ 3-state auth: anonymous / authenticated / reject │ │
│ │ Read: list_agents, health_check, │ │
│ │ get_blocked_requests, get_agent_card, │ │
│ │ get_aggregated_card, get_rate_limit_status, │ │
│ │ list_policies, evaluate_policy, │ │
│ │ list_pending_changes │ │
│ │ Write: update_rate_limit, register_agent, │ │
│ │ deregister_agent, send_test_message, │ │
│ │ approve_card_change, reject_card_change │ │
│ └─────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
MCP management in action (list agents, health check, agent cards):
Security (2-layer pipeline):
- Pre-auth IP rate limiting (global_rate_limit on listen port)
- Authentication (JWT, API Key, or passthrough modes)
- Post-auth user rate limiting (per-user bucket)
Protocol Detector: Identifies incoming request as JSON-RPC, REST, or Agent Card fetch based on method/path.
Router:
path-prefix:/agents/{name}/→ agent namednamesingle: all traffic → one default agent
Policy Engine (ABAC): Attribute-based access control with priority-ordered rules. Supports IP, user, agent, method, time-based, and header conditions. Rules are hot-reloadable via config reload.
Proxy:
- HTTP: Standard A2A JSON-RPC and REST binding forwarding
- SSE: Maintains goroutine per stream, demuxes chunks, gracefully drains on shutdown
- gRPC: Accepts A2A gRPC calls on a separate port, translates to/from JSON-RPC for backend agents
Agent Card Manager:
- Polls each agent's
/.well-known/agent.json(configurable interval) - Caches responses, detects changes
- Aggregates into merged card at
/agents/.well-known/agent.json - Validates JWS signatures if configured
Audit Logger:
- OTel-compatible structured JSON
- Records: timestamp, method, agent, user_id, decision (allow/block), reason, rate_limit_state
- Configurable sampling (default 100% for errors, 1% for allow)
Health Checks:
/healthz: gateway status (startup/running/shutdown)/readyz: all agents health + gateway readiness (modes:any_healthy,default_healthy,all_healthy)
agents:
- name: echo
url: http://echo-agent:9000
default: true
- name: streaming
url: http://streaming-agent:9001
security:
auth:
mode: passthrough-strict
rate_limit:
enabled: true
routing:
mode: path-prefix
logging:
level: info
format: json# Development profile (loose security for testing)
./sentinel init --profile dev
# Production profile (strict security defaults)
./sentinel init --profile prod./sentinel validate --config sentinel.yaml
# Output: config validlisten:
grpc_port: 8443 # Separate port for gRPC connections
grpc:
enabled: true
max_message_size: 4MB # Max gRPC message size
reflection: true # Enable gRPC server reflectiongRPC clients connect on the gRPC port. Sentinel translates A2A gRPC calls to JSON-RPC internally and forwards to backend agents over HTTP. No gRPC support is required from agents.
reload:
enabled: true
watch: true # Enable fsnotify file watching
debounce: 2s # Debounce interval for file changesSend SIGHUP to the sentinel process or rely on automatic file watching. Only reloadable fields (rate limits, policies, logging, agents) are updated. Non-reloadable fields (listen ports, TLS, auth mode) require a restart.
security:
policies:
- name: block-internal-ips
priority: 10
effect: deny
conditions:
source_ip:
cidr: ["192.168.0.0/16"]
- name: business-hours-only
priority: 20
effect: deny
conditions:
time:
outside: "09:00-17:00"
timezone: "America/New_York"
- name: restrict-agent-access
priority: 30
effect: deny
conditions:
agent: ["internal-agent"]
user_not: ["admin@example.com"]See docs/SECURITY.md for full policy engine documentation.
See sentinel.yaml.example for all available options including:
- agents: Health checks, polling intervals, timeouts, max concurrent streams
- security.auth: JWT issuer/audience/jwks_url, API key validation, passthrough modes
- security.rate_limit: IP limits, user limits, per-agent limits, cleanup intervals
- security.replay: Nonce tracking (memory or Redis), configurable window, nonce_source, clock_skew
- security.push: SSRF defense (block private networks), allowed domains, HMAC signing, dns_fail_policy
- security.policies: ABAC rules with IP, user, agent, method, time, header conditions
- body_inspection: Max body size, skip for streaming requests
- card: Aggregation mode, JWK file for signing
- logging: Audit sampling, max body log size, output format
- grpc: gRPC binding port, max message size, reflection
- reload: Hot-reload settings (watch, debounce)
- mcp: Port, auth token, enabled flag (MCP 2025-11-25 Streamable HTTP)
| Mode | Behavior | Use Case |
|---|---|---|
passthrough |
Accept with or without auth headers | Development |
passthrough-strict |
Default. Require auth headers but don't validate | Strict development |
jwt |
Validate JWT (issuer, audience, JWKS) | Production with token issuers |
api-key |
Simple shared secret | Simple production |
none |
No auth (use only if behind trusted proxy) | Internal networks only |
All modes include a hint and docs_url in error responses to guide users toward fixes.
Pre-auth (per IP):
- Global limit on listen port (early drop, no CPU spent on auth)
- Configured via
listen.global_rate_limit
Post-auth (per user):
- Per-user bucket after authentication
- Configured via
security.rate_limit.user.per_userand.burst
Both layers return 429 with remaining window:
{
"error": {
"code": 429,
"message": "Rate limit exceeded",
"hint": "Current limit: 100 req/min. Wait 30s or contact admin.",
"docs_url": "https://a2a-sentinel.dev/docs/rate-limit"
}
}All decisions (allow/block) are logged in OTel-compatible format:
{
"timestamp": "2025-02-26T12:34:56Z",
"level": "info",
"msg": "request_decision",
"http_method": "POST",
"http_target": "/agents/echo/",
"agent_name": "echo",
"user_id": "user-123",
"decision": "allow",
"reason": "rate_limit_ok",
"rate_limit_state": {
"user_remaining": 95,
"user_reset_secs": 59
}
}Configurable sampling rates reduce noise in high-volume environments.
Deploy a2a-sentinel to Kubernetes using the included Helm chart:
# Install from local chart
helm install sentinel deploy/helm/a2a-sentinel/ \
--namespace sentinel-system \
--create-namespace \
--set config.agents[0].name=echo \
--set config.agents[0].url=http://echo-agent:9000
# Or with a values file
helm install sentinel deploy/helm/a2a-sentinel/ \
--namespace sentinel-system \
--create-namespace \
-f my-values.yaml
# Upgrade
helm upgrade sentinel deploy/helm/a2a-sentinel/ \
--namespace sentinel-system \
-f my-values.yamlThe chart includes:
- Deployment with configurable replicas and resource limits
- Service for HTTP (8080) and gRPC (8443) ports
- ConfigMap for sentinel.yaml
- Optional ServiceMonitor for Prometheus scraping
- Optional Ingress resource
- Pod disruption budget for high availability
See deploy/helm/a2a-sentinel/values.yaml for all chart configuration options.
- Go 1.22+
- git
go build -o sentinel ./cmd/sentinelgo test -race ./...All code includes _test.go files covering happy path, error conditions, and concurrent scenarios.
# Serve with config
./sentinel --config sentinel.yaml serve
# Validate before serving
./sentinel --config sentinel.yaml validate
# Generate config template
./sentinel init --profile dev
# Migrate config to agentgateway format
./sentinel migrate --to agentgateway --output agentgateway.yaml
# Show version
./sentinel --version
# Show help
./sentinel helpa2a-sentinel/
├── cmd/sentinel/
│ ├── main.go # CLI entrypoint (serve, validate, init, migrate)
│ └── main_test.go
├── proto/
│ └── a2a/v1/ # A2A gRPC service definitions (.proto)
├── gen/
│ └── a2a/v1/ # Generated Go code from proto definitions
├── internal/
│ ├── config/ # YAML parsing, validation, dev/prod profiles, hot-reload
│ ├── ctxkeys/ # context.Context key definitions
│ ├── errors/ # SentinelError type + HTTP/JSON-RPC/gRPC mapping
│ ├── health/ # /healthz, /readyz handlers
│ ├── server/ # HTTP server integration, graceful shutdown
│ ├── grpc/ # gRPC server, interceptors, JSON-RPC translation
│ ├── protocol/ # A2A types, Protocol Detector, body inspection
│ ├── security/ # Auth, rate limiting, SSRF defense, policy engine
│ ├── proxy/ # HTTP, SSE, and gRPC proxies (no ReverseProxy)
│ ├── router/ # path-prefix and single-agent routing
│ ├── agentcard/ # Agent Card polling, caching, aggregation
│ ├── audit/ # OTel-compatible audit logging, Prometheus metrics
│ └── mcpserver/ # MCP server (15 tools, read + write + card + policy)
├── deploy/
│ └── helm/a2a-sentinel/ # Helm chart for Kubernetes deployment
├── examples/
│ ├── echo-agent/ # Synchronous demo agent (Python)
│ ├── streaming-agent/ # SSE streaming demo agent (Python)
│ └── grafana/ # Grafana dashboard JSON
├── docs/
│ ├── ARCHITECTURE.md # System architecture and request flow
│ ├── SECURITY.md # Security model and threat defenses
│ ├── ERRORS.md # Error catalog and troubleshooting
│ └── MIGRATION.md # Migration guide to agentgateway
├── docker-compose.yaml # Local development stack
├── sentinel.yaml.example # Full configuration reference
├── CHANGELOG.md # Version history
└── README.md # This file
All changes follow Test-Driven Development:
- Red: Write test covering new behavior
- Green: Implement minimum code to pass test
- Refactor: Clean up, remove duplication
- Verify: Run
go test -race ./...for full suite
Example:
# Write test in internal/security/ratelimit_test.go
# Run tests until failure
go test -race ./internal/security/...
# Implement rate limiter
# Run until green
go test -race ./internal/security/...
# Verify full suite
go test -race ./...- Read CONTRIBUTING.md for coding standards
- Read docs/ARCHITECTURE.md for detailed architecture
- Write tests first, then implementation
- Ensure
go test -race ./...passes - Keep error messages educational (include
hintanddocs_url) - Don't inject sentinel-specific headers into backend requests
- Use
internal/ctxkeys/for all context keys (no direct key definitions)
When you're ready to move to production infrastructure, migrate to agentgateway (Linux Foundation):
No agent code changes required. Both sentinel and agentgateway use the same A2A protocol and expect the same Agent Card format. Your agents work with either gateway out of the box.
Use the sentinel migrate command to generate agentgateway-compatible config from your existing sentinel.yaml:
sentinel migrate --to agentgateway --output agentgateway.yamlSee docs/MIGRATION.md for the full migration guide.
v0.1
- Core gateway (HTTP/SSE proxy)
- 2-layer rate limiting + authentication
- Agent Card caching with change detection
- Structured audit logging (OTel format)
- Health checks (/healthz, /readyz)
- MCP server (read-only, 3 tools)
v0.2
- Full MCP server (13 tools: read + write + card approval)
- JWS Agent Card signature verification
- SSRF protection for push notifications
- Replay attack prevention (nonce + timestamp)
sentinel migratecommand for agentgateway- Card change approve mode (MCP-based workflow)
- Prometheus-compatible metrics endpoint (/metrics)
- Security integration test suite
v0.3 (Current)
- gRPC binding support with JSON-RPC protocol translation
- Config hot-reload (SIGHUP + fsnotify file watch with debounce)
- Extended Prometheus metrics with histograms (prometheus/client_golang)
- Grafana dashboard example
- Helm chart for Kubernetes deployment
- ABAC policy engine (IP, user, agent, method, time-based, header rules)
- Policy evaluation MCP tools
- Replay detection: nonce_source priority + timestamp validation with clock_skew (v0.3.1)
- SSRF checker: configurable dns_fail_policy for DNS failures (v0.3.1)
- Consistent HTTP 502 handling across all error mapping systems (v0.3.1)
v1.0 (Planned)
- OPA policy integration
- OTel SDK integration (Jaeger, Datadog)
- Multi-tenancy support
- A2A Technology Compatibility Kit (TCK) integration
Q: Gateway starts but agents show unhealthy?
- Check agent URLs in config
- Verify agents are running and respond to
/.well-known/agent.json - Check
docker compose logsfor connection errors
Q: Rate limit errors on every request?
- Check
listen.global_rate_limitis reasonable (default 5000/min) - Check
security.rate_limit.user.per_user(default 100/min) - Look at audit logs to see which limit is triggering
Q: SSE streams disconnecting unexpectedly?
- Check
server.shutdown.drain_timeout(default 15s) - Verify backend agent keeps stream open
- Check proxy logs for timeout errors
Q: MCP server won't start?
- Ensure
mcp.enabled: truein config - Check if port 8081 is available
- MCP server only listens on 127.0.0.1 (not externally exposed)
- Configuration reference: sentinel.yaml.example
- Architecture & design: docs/ARCHITECTURE.md
- Security: docs/SECURITY.md
- Error reference: docs/ERRORS.md
- Migration guide: docs/MIGRATION.md
- Changelog: CHANGELOG.md
- A2A Protocol spec: https://a2a-protocol.org/latest/specification/
- 프로젝트 소개: docs/ko/README.ko.md
- 아키텍처 설계: docs/ko/ARCHITECTURE.ko.md
- 보안 가이드: docs/ko/SECURITY.ko.md
Open an issue on GitHub with:
- Config file (sanitize sensitive values)
- Error logs from
docker compose logs sentinel - Steps to reproduce
- Expected vs actual behavior
Apache License 2.0 — See LICENSE for details.
- A2A Protocol: Linux Foundation (Google, Microsoft, AWS, Salesforce, SAP)
- Built with: Claude Code — AI-assisted development
- Inspiration: agentgateway (agentgateway is the production platform; sentinel is the developer's gateway)
Made with intention for developers who want security by default, not by deployment.



