Auth & Security
cPod uses OAuth 2.0 client_credentials (RFC 6749 §4.4) for all service-to-service authentication. cpod-backend owns the auth surface — it validates tokens and proxies to the CoreSDK sidecar for policy enforcement. The sidecar binds to loopback only and is never reachable from the network.
Your App → POST /api/v1/auth/token → cpod-backend → gRPC [::]50051 → CoreSDK Sidecar
↑
Registers apps, issues tokens
Sidecar port never exposed
client_credentials Flow#
Used by all backend services, CI, and MCP-registered apps.
Your Service
│
│ POST /api/v1/auth/token
│ Content-Type: application/x-www-form-urlencoded
│ grant_type=client_credentials
│ &client_id=app_a1b2c3d4
│ &client_secret=sk-xxxx
│ &scope=mcp:call mcp:register
▼
cpod-backend
│
│ bcrypt-verify client_secret
│ Lookup tenant_id from app_service_accounts
│ Issue HS256 JWT (exp = now + 3600)
▼
Service receives access_token
Caches it, refreshes at 80% TTL (48 min)
Uses: Authorization: Bearer <jwt>
See Authentication for SDK helpers and the token cache pattern.
JWT Structure#
| Claim | Value |
|---|---|
sub | app_id — the registered app identifier |
tenant_id | Tenant owning the app — extracted server-side, never client-supplied |
roles | ["app_service_account"] |
scope | Requested scopes (e.g. mcp:call mcp:register) |
iat | Issued at (Unix timestamp) |
exp | Expiry (Unix timestamp, 1 hour after iat) |
tenant_id is embedded at issuance. It is re-derived on every request from the validated JWT — the client cannot override it via request args or headers.
Scopes#
| Scope | What it grants |
|---|---|
mcp:call | Invoke tools through the MCP proxy |
mcp:register | Register and manage MCP tools |
admin:* | Full administrative access — use sparingly |
Tokens issued via client_credentials carry the app_service_account role. This role cannot elevate to tenant_admin or global_admin.
Token Validation Pipeline#
On every request, cpod-backend middleware validates the Bearer token before any handler runs:
cpod-backend receives: Authorization: Bearer eyJ...
│
▼
┌─ LocalFallbackSDK.authorize ────────────────┐
│ 1. Decode JWT header │
│ 2. Verify HS256 signature │
│ 3. Check exp > now │
│ 4. Extract tenant_id, roles, scope │
└──────────────────┬──────────────────────────┘
valid │ invalid → 401
▼
Handler executes
(tenantId stamped from claims)
If the CoreSDK sidecar is available, token validation is done via gRPC ValidateToken instead — the sidecar uses RS256 with rotating JWKS.
CoreSDK Sidecar Security Properties#
| Property | Detail |
|---|---|
| Language | Rust — memory-safe |
| Binds to | [::1]:50051 — loopback only, not routable |
| Algorithm | RS256 (production) / HS256 fallback (development without sidecar) |
| Policy | Rego evaluation via AuthService.Authorize |
| JWKS refresh | Every 30 seconds from the Control Plane |
| State | Stateless at rest — in-memory caches only |
In development without a sidecar, cpod-backend falls back to local HS256 validation using BOOTSTRAP_ADMIN_SECRET. Always configure the CoreSDK sidecar (CORESDK_SIDECAR_ADDR) in staging and production.
Tenant Isolation#
tenant_id is always derived from the validated JWT — never from request arguments. A token issued for tenant A can only read data scoped to tenant A, regardless of what the client sends in the request body or query string.
client sends: POST /api/v1/mcp/call { "appId": "...", "tenantId": "tenant-B" }
│
cpod-backend ignores "tenantId" in body
Extracts tenant_id from JWT claims only
Scopes lookup to JWT tenant_id