e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
363 lines
10 KiB
Markdown
363 lines
10 KiB
Markdown
---
|
|
layout: default
|
|
---
|
|
|
|
# MCP Security Guide
|
|
|
|
This guide covers how to secure your MCP gateway for production use, including authentication, per-tool scopes, rate limiting, and audit logging.
|
|
|
|
## Overview
|
|
|
|
The MCP gateway provides four layers of security:
|
|
|
|
1. **Authentication** - Verify the caller's identity via bearer tokens
|
|
2. **Scopes** - Control which tools each token can access
|
|
3. **Rate Limiting** - Prevent abuse with per-tool rate limits
|
|
4. **Audit Logging** - Record every tool call for compliance and debugging
|
|
|
|
## Authentication
|
|
|
|
### Bearer Token Auth
|
|
|
|
The MCP gateway uses bearer token authentication. Tokens are validated by the configured `auth.Auth` provider.
|
|
|
|
```go
|
|
import (
|
|
"go-micro.dev/v6/gateway/mcp"
|
|
"go-micro.dev/v6/auth"
|
|
)
|
|
|
|
gateway := mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: service.Options().Registry,
|
|
Auth: authProvider, // auth.Auth implementation
|
|
})
|
|
```
|
|
|
|
Agents pass tokens in the `Authorization` header:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3000/mcp/call \
|
|
-H "Authorization: Bearer <token>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"tool": "tasks.TaskService.Create", "input": {"title": "New task"}}'
|
|
```
|
|
|
|
### Using micro run / micro server
|
|
|
|
When using `micro run` or `micro server`, authentication is handled automatically:
|
|
|
|
- **Development mode (`micro run`):** Auth is disabled by default for easy development
|
|
- **Production mode (`micro server`):** JWT auth is enabled with user management at `/auth`
|
|
|
|
Create tokens with specific scopes via the dashboard at `/auth/tokens`.
|
|
|
|
## Per-Tool Scopes
|
|
|
|
Scopes control which tools a token can access. There are two ways to set scopes.
|
|
|
|
### Service-Level Scopes
|
|
|
|
Set scopes when registering your handler. These travel with the service through the registry:
|
|
|
|
```go
|
|
handler := service.Server().NewHandler(
|
|
new(TaskService),
|
|
server.WithEndpointScopes("TaskService.Get", "tasks:read"),
|
|
server.WithEndpointScopes("TaskService.List", "tasks:read"),
|
|
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
|
server.WithEndpointScopes("TaskService.Update", "tasks:write"),
|
|
server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
|
|
)
|
|
```
|
|
|
|
### Gateway-Level Scopes
|
|
|
|
Override or add scopes at the gateway without modifying services. Gateway scopes take precedence:
|
|
|
|
```go
|
|
mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: reg,
|
|
Auth: authProvider,
|
|
Scopes: map[string][]string{
|
|
"tasks.TaskService.Create": {"tasks:write"},
|
|
"tasks.TaskService.Delete": {"tasks:admin"},
|
|
"billing.Billing.Charge": {"billing:admin"},
|
|
},
|
|
})
|
|
```
|
|
|
|
### Scope Enforcement
|
|
|
|
When a tool is called:
|
|
|
|
1. Gateway checks if the tool has required scopes
|
|
2. If scopes are defined, the caller's token must include at least one matching scope
|
|
3. A token with scope `*` has unrestricted access (admin)
|
|
4. If no scopes are defined for a tool, any authenticated token can call it
|
|
5. Denied calls return `403 Forbidden`
|
|
|
|
### Common Scope Patterns
|
|
|
|
| Pattern | Use Case |
|
|
|---------|----------|
|
|
| `service:read` | Read-only access to a service |
|
|
| `service:write` | Create and update operations |
|
|
| `service:admin` | Delete and destructive operations |
|
|
| `*` | Full admin access (use sparingly) |
|
|
| `internal` | Internal-only tools not exposed to external agents |
|
|
|
|
### Token Examples
|
|
|
|
```
|
|
Token A: scopes=["tasks:read"]
|
|
✅ Can call TaskService.Get, TaskService.List
|
|
❌ Cannot call TaskService.Create, TaskService.Delete
|
|
|
|
Token B: scopes=["tasks:read", "tasks:write"]
|
|
✅ Can call Get, List, Create, Update
|
|
❌ Cannot call TaskService.Delete (needs tasks:admin)
|
|
|
|
Token C: scopes=["*"]
|
|
✅ Can call everything (admin)
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
Prevent abuse with per-tool rate limiting using a token bucket algorithm:
|
|
|
|
```go
|
|
mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: reg,
|
|
RateLimit: &mcp.RateLimitConfig{
|
|
RequestsPerSecond: 10, // Sustained rate
|
|
Burst: 20, // Allow bursts up to 20
|
|
},
|
|
})
|
|
```
|
|
|
|
When the rate limit is exceeded, calls return `429 Too Many Requests`.
|
|
|
|
### Choosing Rate Limits
|
|
|
|
| Service Type | Requests/sec | Burst | Rationale |
|
|
|-------------|-------------|-------|-----------|
|
|
| Read-heavy API | 50 | 100 | High throughput, low cost |
|
|
| Write API | 10 | 20 | Moderate, prevents spam |
|
|
| Expensive operation | 2 | 5 | Protect downstream resources |
|
|
| Internal tool | 100 | 200 | Trusted callers, higher limits |
|
|
|
|
## Audit Logging
|
|
|
|
Record every tool call for compliance, debugging, and analytics:
|
|
|
|
```go
|
|
mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: reg,
|
|
Auth: authProvider,
|
|
AuditFunc: func(record mcp.AuditRecord) {
|
|
log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v err=%v",
|
|
record.Tool,
|
|
record.AccountID,
|
|
record.Allowed,
|
|
record.Duration,
|
|
record.Error,
|
|
)
|
|
},
|
|
})
|
|
```
|
|
|
|
### AuditRecord Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `Tool` | `string` | Full tool name (e.g., `tasks.TaskService.Create`) |
|
|
| `AccountID` | `string` | Caller's account ID from the auth token |
|
|
| `Scopes` | `[]string` | Scopes on the caller's token |
|
|
| `Allowed` | `bool` | Whether the call was permitted |
|
|
| `Duration` | `time.Duration` | How long the call took |
|
|
| `Error` | `error` | Error if the call failed |
|
|
| `TraceID` | `string` | UUID trace ID for correlation |
|
|
| `DeniedReason` | `string` | Why the call was denied (empty if allowed) |
|
|
|
|
### Production Audit Logging
|
|
|
|
For production, send audit records to a structured logging system:
|
|
|
|
```go
|
|
AuditFunc: func(r mcp.AuditRecord) {
|
|
// Structured JSON logging
|
|
logger.Info("mcp_tool_call",
|
|
"tool", r.Tool,
|
|
"account", r.AccountID,
|
|
"allowed", r.Allowed,
|
|
"duration_ms", r.Duration.Milliseconds(),
|
|
"trace_id", r.TraceID,
|
|
)
|
|
|
|
// Alert on denied calls
|
|
if !r.Allowed {
|
|
alerting.Notify("MCP access denied",
|
|
"tool", r.Tool,
|
|
"account", r.AccountID,
|
|
)
|
|
}
|
|
},
|
|
```
|
|
|
|
## Tracing
|
|
|
|
Every MCP tool call gets a UUID trace ID, propagated via metadata headers:
|
|
|
|
| Header | Description |
|
|
|--------|-------------|
|
|
| `Mcp-Trace-Id` | UUID for the tool call |
|
|
| `Mcp-Tool-Name` | Name of the tool called |
|
|
| `Mcp-Account-Id` | Caller's account ID |
|
|
|
|
These are available in your handler via context metadata:
|
|
|
|
```go
|
|
func (t *TaskService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
|
md, _ := metadata.FromContext(ctx)
|
|
traceID := md["Mcp-Trace-Id"]
|
|
log.Printf("Creating task, trace: %s", traceID)
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### OpenTelemetry Integration
|
|
|
|
For full distributed tracing, plug in an OpenTelemetry trace provider:
|
|
|
|
```go
|
|
import (
|
|
"go.opentelemetry.io/otel"
|
|
"go-micro.dev/v6/gateway/mcp"
|
|
)
|
|
|
|
mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: reg,
|
|
TraceProvider: otel.GetTracerProvider(),
|
|
})
|
|
```
|
|
|
|
Each tool call creates a span (`mcp.tool.call`) with these attributes:
|
|
|
|
| Attribute | Example |
|
|
|-----------|---------|
|
|
| `mcp.tool.name` | `tasks.TaskService.Create` |
|
|
| `mcp.transport` | `http`, `websocket`, `stdio` |
|
|
| `mcp.account.id` | `user-123` |
|
|
| `mcp.trace.id` | `a1b2c3d4-...` |
|
|
| `mcp.auth.allowed` | `true` |
|
|
| `mcp.auth.denied_reason` | `insufficient_scope` |
|
|
| `mcp.scopes.required` | `tasks:write` |
|
|
| `mcp.rate_limited` | `false` |
|
|
|
|
The gateway propagates W3C trace context downstream, so you get end-to-end traces from agent → gateway → service in Jaeger, Zipkin, or any OTel-compatible backend.
|
|
|
|
## WebSocket Authentication
|
|
|
|
The WebSocket transport supports two authentication methods:
|
|
|
|
### Connection-Level Auth (Recommended)
|
|
|
|
Pass the token in the WebSocket upgrade request:
|
|
|
|
```javascript
|
|
const ws = new WebSocket("ws://localhost:3000/mcp/ws", {
|
|
headers: { "Authorization": "Bearer <token>" }
|
|
});
|
|
```
|
|
|
|
The token is validated once on connection and applies to all messages on that connection.
|
|
|
|
### Per-Message Auth
|
|
|
|
For stateless connections, pass a `_token` parameter with each tool call:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "tasks.TaskService.Create",
|
|
"arguments": {"title": "New task"},
|
|
"_token": "Bearer <token>"
|
|
}
|
|
}
|
|
```
|
|
|
|
Connection-level auth takes precedence over per-message auth.
|
|
|
|
## Production Checklist
|
|
|
|
Before deploying MCP to production:
|
|
|
|
- [ ] **Auth enabled** - Configure an `auth.Auth` provider
|
|
- [ ] **Scopes defined** - Every write/delete endpoint has required scopes
|
|
- [ ] **Rate limits set** - Appropriate limits for each service type
|
|
- [ ] **Audit logging active** - All calls logged to a persistent store
|
|
- [ ] **HTTPS/TLS** - MCP gateway behind TLS termination
|
|
- [ ] **Token rotation** - Process for rotating compromised tokens
|
|
- [ ] **Monitoring** - Alerts on high error rates or denied calls
|
|
- [ ] **Testing** - Verified scope enforcement with `micro mcp test`
|
|
|
|
## Full Example
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
|
|
"go-micro.dev/v6"
|
|
"go-micro.dev/v6/auth"
|
|
"go-micro.dev/v6/gateway/mcp"
|
|
"go-micro.dev/v6/server"
|
|
)
|
|
|
|
func main() {
|
|
service := micro.NewService("tasks",
|
|
micro.Address(":8081"),
|
|
)
|
|
service.Init()
|
|
|
|
// Register handler with scopes
|
|
handler := service.Server().NewHandler(
|
|
&TaskService{tasks: make(map[string]*Task)},
|
|
server.WithEndpointScopes("TaskService.Get", "tasks:read"),
|
|
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
|
|
server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
|
|
)
|
|
service.Server().Handle(handler)
|
|
|
|
// Start MCP gateway with full security
|
|
go mcp.ListenAndServe(":3000", mcp.Options{
|
|
Registry: service.Options().Registry,
|
|
Auth: service.Options().Auth,
|
|
Scopes: map[string][]string{
|
|
// Gateway-level overrides
|
|
"billing.Billing.Charge": {"billing:admin"},
|
|
},
|
|
RateLimit: &mcp.RateLimitConfig{
|
|
RequestsPerSecond: 10,
|
|
Burst: 20,
|
|
},
|
|
AuditFunc: func(r mcp.AuditRecord) {
|
|
log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v",
|
|
r.Tool, r.AccountID, r.Allowed, r.Duration)
|
|
},
|
|
})
|
|
|
|
service.Run()
|
|
}
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
- [Building AI-Native Services](ai-native-services.md) - End-to-end tutorial
|
|
- [Tool Description Best Practices](tool-descriptions.md) - Write effective documentation
|
|
- [Agent Integration Patterns](agent-patterns.md) - Multi-agent architectures
|