Security Configuration
This guide covers the security settings introduced in the Team Coca P0 Security Fixes (ADR-018).
All security features are enabled by default with secure settings. You only need to configure them if you're customizing for your environment.
Quick Reference
| Feature | Setting | Default | Rollback |
|---|---|---|---|
| JWT Audience | ALLOWED_AUDIENCES | stoa-mcp-gateway,account | Set to "" |
| CORS | CORS_ORIGINS | Whitelist | Set to "*" |
| SSE Limits | SSE_LIMITER_ENABLED | true | Set to false |
| Trusted Proxies | SSE_TRUSTED_PROXIES | "" (none) | Configure CIDRs |
JWT Audience Validation (CAB-938)
What It Does
Validates that JWT tokens were issued specifically for the MCP Gateway, preventing token reuse from other Keycloak clients.
Configuration
# Environment variables
ALLOWED_AUDIENCES=stoa-mcp-gateway,account
# Or in settings.py
allowed_audiences: str = "stoa-mcp-gateway,account"
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
ALLOWED_AUDIENCES | string | stoa-mcp-gateway,account | Comma-separated list of valid JWT audiences |
Keycloak Configuration
Ensure your Keycloak client is configured to include the audience:
{
"clientId": "stoa-mcp-gateway",
"protocolMappers": [
{
"name": "audience-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"included.client.audience": "stoa-mcp-gateway",
"access.token.claim": "true"
}
}
]
}
Testing
# Decode a token and check the audience
echo $TOKEN | cut -d. -f2 | base64 -d | jq '.aud'
# Should include: "stoa-mcp-gateway"
Rollback
If JWT validation breaks authentication:
# Disable audience validation (temporary!)
export ALLOWED_AUDIENCES=""
Disabling audience validation is a security risk. Only use for debugging.
CORS Configuration (CAB-950)
What It Does
Restricts which origins can make cross-origin requests to the API, preventing CSRF and data exfiltration attacks.
Configuration
# Environment variables
CORS_ORIGINS=https://console.stoa.dev,https://portal.stoa.dev
CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Request-ID,X-Tenant-ID
CORS_EXPOSE_HEADERS=X-Request-ID,X-Trace-ID
CORS_MAX_AGE=600
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
CORS_ORIGINS | string | See below | Comma-separated allowed origins |
CORS_ALLOW_METHODS | string | GET,POST,PUT,DELETE,OPTIONS | Allowed HTTP methods |
CORS_ALLOW_HEADERS | string | Authorization,Content-Type,... | Allowed request headers |
CORS_EXPOSE_HEADERS | string | X-Request-ID,X-Trace-ID | Headers exposed to browser |
CORS_MAX_AGE | int | 600 | Preflight cache duration (seconds) |
Default Origins
https://console.stoa.dev
https://portal.stoa.dev
https://console.gostoa.dev
https://portal.gostoa.dev
Local Development
For local development, add localhost origins:
CORS_ORIGINS=https://console.stoa.dev,http://localhost:3000,http://localhost:5173
Testing
# Test CORS preflight
curl -X OPTIONS ${STOA_API_URL}/v1/tools \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-v
# Should return 403 or missing Access-Control-Allow-Origin
Rollback
# Allow all origins (NOT RECOMMENDED!)
export CORS_ORIGINS="*"
Never use CORS_ORIGINS="*" in production. This allows any website to make authenticated requests to your API.
SSE Connection Limits (CAB-939)
What It Does
Prevents connection exhaustion attacks (Slowloris) by limiting SSE connections per IP, per tenant, and globally.
Configuration
# Environment variables
SSE_LIMITER_ENABLED=true
SSE_TRUSTED_PROXIES=10.100.0.0/16 # Your cluster CIDR
Settings
| Setting | Type | Default | Description |
|---|---|---|---|
SSE_LIMITER_ENABLED | bool | true | Enable/disable rate limiting |
SSE_TRUSTED_PROXIES | string | "" | Trusted proxy CIDRs for X-Forwarded-For |
Built-in Limits
These limits are hardcoded but can be adjusted via code:
| Limit | Value | Description |
|---|---|---|
MAX_PER_IP | 10 | Max connections from single IP |
MAX_PER_TENANT | 100 | Max connections per tenant |
MAX_TOTAL | 5000 | Global connection limit |
IDLE_TIMEOUT | 30s | Close idle connections after |
MAX_DURATION | 1h | Maximum connection lifetime |
RATE_LIMIT_PER_MIN | 5 | New connections per IP per minute |
Trusted Proxies
If you're behind a load balancer or ingress controller, you must configure trusted proxies to get accurate client IPs.
# EKS example
SSE_TRUSTED_PROXIES=10.100.0.0/16
# GKE example
SSE_TRUSTED_PROXIES=10.0.0.0/8
# Multiple CIDRs
SSE_TRUSTED_PROXIES=10.100.0.0/16,172.16.0.0/12
If not configured, the limiter will use the direct connection IP (which may be your load balancer).
Testing
# Test rate limit (should get 429 on 11th connection)
for i in {1..15}; do
curl -N "${STOA_API_URL}/mcp/sse" \
-H "Authorization: Bearer $TOKEN" &
done
# Check active connections (if you have access)
curl ${STOA_API_URL}/metrics | grep stoa_sse
Response Codes
| Code | Meaning | Header |
|---|---|---|
| 429 | Too Many Requests | Retry-After: 60 |
Rollback
# Disable SSE rate limiting
export SSE_LIMITER_ENABLED=false
Container Security (CAB-945)
Pod Security Standards
STOA enforces the restricted Pod Security Standard on the stoa-system namespace:
apiVersion: v1
kind: Namespace
metadata:
name: stoa-system
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Deployment Security Context
All STOA deployments use this security context:
spec:
automountServiceAccountToken: false # Unless needed
securityContext:
runAsNonRoot: true
fsGroup: 1000
containers:
- securityContext:
runAsUser: 1000
runAsGroup: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
Network Policies
The control plane is isolated with NetworkPolicy:
# Ingress: Only from MCP Gateway and Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: stoa-mcp-gateway
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
# Egress: Only to known services
egress:
- to:
- podSelector:
matchLabels:
app: postgresql
- to:
- podSelector:
matchLabels:
app: redis
# ... and DNS
Verification
# Check PSS enforcement
kubectl get ns stoa-system -o jsonpath='{.metadata.labels}' | jq
# Check security context
kubectl get pod -n stoa-system -o jsonpath='{.items[0].spec.securityContext}'
# Test NetworkPolicy (should fail from random pod)
kubectl run test --rm -it --image=alpine -- wget -O- http://stoa-control-plane:8080
Monitoring
Prometheus Metrics (P1)
These metrics will be added in a follow-up release:
# SSE active connections
stoa_sse_connections_total{tenant="acme"}
# SSE rejected connections
stoa_sse_rejected_total{reason="ip_limit"}
# JWT validation failures
stoa_jwt_validation_failures_total{reason="invalid_audience"}
Alerts
Recommended alerts:
- alert: STOAHighSSERejectionRate
expr: rate(stoa_sse_rejected_total[5m]) > 10
labels:
severity: warning
annotations:
summary: High SSE rejection rate (possible attack)
- alert: STOAJWTValidationFailures
expr: rate(stoa_jwt_validation_failures_total[5m]) > 5
labels:
severity: warning
annotations:
summary: JWT validation failures detected
Troubleshooting
JWT Validation Errors
Symptom: 401 Unauthorized with message "Invalid audience"
Solution:
- Check token audience:
echo $TOKEN | cut -d. -f2 | base64 -d | jq '.aud' - Verify Keycloak client mapper configuration
- Temporarily disable:
ALLOWED_AUDIENCES=""
CORS Errors
Symptom: Browser console shows "CORS policy" errors
Solution:
- Check origin is in whitelist:
echo $CORS_ORIGINS - Add origin:
CORS_ORIGINS="...,https://your-domain.com" - Check for typos (http vs https)
SSE Rate Limiting
Symptom: 429 Too Many Requests with Retry-After: 60
Solution:
- Check if legitimate traffic or attack
- Verify client IP detection:
curl -v ... | grep X-Forwarded-For - Configure trusted proxies:
SSE_TRUSTED_PROXIES=... - Temporarily disable:
SSE_LIMITER_ENABLED=false
Container Security
Symptom: Pod fails to start with "container has runAsNonRoot and image will run as root"
Solution:
- Use non-root base image
- Add
USER 1000to Dockerfile - Set
runAsUserin deployment
References
- ADR-018: Security Hardening P0
- OWASP API Security Top 10
- Kubernetes Pod Security Standards
- JWT Best Practices (RFC 8725)
Last updated: 2026-01-25 — Team Coca Security Audit 🍫