Gateway Hardening¶
The MeshFlows gateway is the single public entrypoint to the platform. This page documents the security controls applied at the edge layer: authentication, security headers, CORS, and rate limiting.
Authentication Modes¶
Every EdgeRoute declares an auth.mode that controls how the gateway enforces caller identity:
| Mode | Behaviour |
|---|---|
none |
No token check. Use only for truly public endpoints. |
token |
Bearer token required (JWT or API key). No scope enforcement. |
oauth2 |
Bearer token required and must carry all required_scopes. Default when auth is omitted. |
Only Bearer tokens are supported (JWT or API key). Basic and HMAC auth are not supported by the gateway today.
JWT Tokens¶
The gateway validates JWT tokens locally (HS256 signature, exp, iss, aud, sub):
- Config env vars:
IDENTITY_JWT_SECRET,IDENTITY_JWT_ISSUER,IDENTITY_JWT_AUDIENCE - On success:
request.state.identityis set with{sub, groups, scopes, token_type: "jwt"}
Live scope refresh (recommended for production)¶
By default (GATEWAY_JWT_SCOPE_MODE=token) scopes are taken from the JWT payload until the token expires.
Set GATEWAY_JWT_SCOPE_MODE=live to refresh scopes and groups from the identity database on each request (with a short cache):
| Variable | Default | Description |
|---|---|---|
GATEWAY_JWT_SCOPE_MODE |
token |
token = JWT claims only; live = refresh via identity GET /auth/me |
GATEWAY_JWT_SCOPE_CACHE_TTL_SECONDS |
15 |
Cache TTL for live scope lookups per token |
IDENTITY_SERVICE_URL |
http://identity:8080 |
Required when mode is live |
Revoked permissions take effect on the gateway within the cache TTL (typically 15s), not when the JWT expires.
If identity is unreachable in live mode, the gateway falls back to JWT token scopes and logs a warning (degraded mode).
Tokens are issued by the identity service at POST /auth/token.
API Keys¶
API keys use a mk_ prefix and are validated via the identity service:
- The gateway calls
POST /api-keys/validateon identity and caches the result for 60 seconds (reduces latency on high-frequency calls) - Keys are stored as SHA-256 hashes; the raw value is shown once at creation
- Scopes work identically to JWT scopes; if a key has no explicit scopes it inherits the owner's permissions
- Config env var:
IDENTITY_SERVICE_URL
Authorization: Bearer mk_live_a1b2c3d4...
Scope Enforcement¶
When auth.mode: oauth2, the route's required_scopes list is checked against the caller's effective scopes:
auth:
mode: oauth2
required_scopes:
- orders:read
- orders:write
A 403 Forbidden is returned if any required scope is missing.
Public Paths¶
The following paths bypass authentication entirely:
GET /healthzGET /readyz
Security Headers¶
Every response from the gateway includes these headers:
| Header | Value |
|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Cache-Control |
no-store |
X-Request-ID |
Correlation ID (generated or forwarded from X-Request-ID request header) |
CORS¶
CORS is configurable per deployment via the GATEWAY_CORS_ORIGINS environment variable (comma-separated list of allowed origins):
GATEWAY_CORS_ORIGINS=https://dashboard.meshflows.org,https://app.example.com
- Preflight
OPTIONSrequests are handled automatically and return204 No Content - If
GATEWAY_CORS_ORIGINSis empty, the header is omitted (same-origin only in browsers) - Allowed methods:
GET, POST, PUT, DELETE, OPTIONS, PATCH - Allowed headers:
Content-Type, Authorization, X-Request-ID
Rate Limiting¶
The gateway supports distributed rate limiting backed by Redis, so limits apply consistently across multiple gateway replicas.
Configuration¶
Rate limits are defined per EdgeRoute:
rate_limit:
max_requests: 100
window_seconds: 60
key_from: subject # ip | subject | api_key | header:<name>
key_from |
Behaviour |
|---|---|
ip |
Limit per client IP |
subject |
Limit per authenticated user/system (sub claim) |
api_key |
Limit per API key |
header:<name> |
Limit per value of a specific header (e.g. header:X-Tenant-ID) |
Redis Backend¶
The gateway uses a fixed-window counter in Redis (INCR + EXPIRE). If Redis is unavailable, the gateway falls back to an in-memory per-pod limiter and logs a warning (at most once per 60 seconds to avoid log spam).
Config env var: REDIS_URL (e.g. redis://redis:6379/0)
Identity Public Surface¶
lab-identity.meshflows.org (HTTPRoute / Traefik) exposes only:
/healthz,/readyz/auth/*(login, OAuth callbacks, MFA challenges,/auth/me)/token(OAuth2 password grant)
IAM admin APIs (/users, /groups, /permissions, …), secrets, products and POST /api-keys/validate are not routed publicly. Cluster services call http://identity:8080 directly.
POST /api-keys/validate additionally requires header X-MeshFlows-Internal-Token (derived from IDENTITY_JWT_SECRET unless IDENTITY_INTERNAL_SERVICE_TOKEN is set). Disable with IDENTITY_REQUIRE_INTERNAL_TOKEN=false (dev only).
Product-Based Rate Limiting¶
For product-based limits see API Keys & Products. The rate_limit_product policy type resolves the caller's quota from their assigned product.
Kubernetes Secrets¶
| Secret | Key | Used by |
|---|---|---|
meshflows-identity-auth |
jwt_secret |
Gateway JWT validation + identity service signing |
meshflows-google-oauth |
client_id, client_secret, redirect_uri, dashboard_callback_url |
Identity service Google OAuth (optional) |
The meshflows-google-oauth secret is created by the deploy pipeline only when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET CI/CD secrets are set. If absent, Google login is simply disabled.