Security
Diminuendo implements a defense-in-depth security architecture. No single layer is trusted exclusively; each defense operates independently and provides protection even if adjacent layers are compromised or misconfigured. The model is concentric: authentication establishes identity, authorization constrains action, transport secures the channel, input validation rejects malformed data, and error sanitization prevents information leakage on the way out. A failure in any one ring does not collapse the others.Authentication
Auth0 JWT Verification
In production, every WebSocket connection must authenticate by sending anauthenticate message with a JWT token. The gateway verifies tokens against Auth0’s JWKS endpoint using the jose library, which handles key rotation automatically — when a key ID in the token does not match the cached keyset, it fetches the latest keys from the endpoint.
JWT Verification Cache
Asymmetric JWT verification (RS256) is computationally expensive — each verification involves parsing the token, fetching the public key from the JWKS set, and performing a cryptographic signature check. To avoid paying this cost on every message from a previously authenticated connection, the gateway maintains an LRU cache of verified tokens:- Cache key: SHA-256 hash of the raw JWT string (via
Bun.CryptoHasher) - Cache size: 10,000 entries maximum
- TTL: Derived from the token’s
expclaim, capped at 5 minutes - Eviction: FIFO when at capacity; expired entries cleaned every 60 seconds
Dev Mode Bypass
WhenDEV_MODE=true, authentication is bypassed entirely. All connections are automatically authenticated as developer@example.com with tenant ID dev. This exists exclusively for local development.
Role-Based Access Control
Roles and Permissions
The RBAC system defines 5 roles with 12 granular permissions. The permission matrix is intentionally conservative — each role receives the minimum set of capabilities required for its function:| Role | session:create | session:read | session:write | session:delete | session:archive | session:steer | member:read | member:write | member:delete | billing:read | billing:write | tenant:admin |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| owner | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| admin | Y | Y | Y | Y | Y | Y | Y | Y | - | Y | - | - |
| billing_admin | Y | Y | Y | - | Y | Y | - | - | - | Y | Y | - |
| member | Y | Y | Y | - | Y | Y | - | - | - | - | - | - |
| viewer | - | Y | - | - | - | - | - | - | - | - | - | - |
MembershipService
Role assignments are stored in per-tenant SQLite databases (tenants/{tenantId}/registry.db). The MembershipService provides CRUD operations on the tenant_members table:
Owner Bootstrap
The first user to authenticate against a tenant is automatically bootstrapped asowner. Subsequent users receive the member role by default. This eliminates the chicken-and-egg problem of tenant provisioning — no admin console is required to create the first privileged account:
Last Owner Protection
The system prevents removing or demoting the last owner of a tenant. BothremoveMember and set_role check the owner count before proceeding — a tenant must always have at least one owner, or the organization becomes unmanageable:
Permission Enforcement
TherequirePermission function is the authorization checkpoint, called before every sensitive operation. It is deliberately simple — a pure function from role and permission to success or failure, with no side effects and no exceptions:
CSRF Protection
Cross-Site Request Forgery protection uses a three-layer defense. Each layer is checked in order; the request passes if any layer succeeds. The layering accommodates the diversity of client types — browsers, CLIs, SDKs — each of which provides different ambient headers:Sec-Fetch-Site Header
If the browser sends
Sec-Fetch-Site: same-origin or Sec-Fetch-Site: none, the request is same-origin and passes immediately. This header cannot be spoofed by JavaScript running in the browser — it is set by the browser itself, making it the strongest signal available.Origin Header
If the
Origin header is present, it is checked against the ALLOWED_ORIGINS configuration list. If absent (non-browser clients such as CLIs and SDKs do not send Origin), the request passes — non-browser clients are not CSRF vectors.SSRF Guard
TheassertSafeUrl function validates that outbound HTTP requests do not target internal networks. It inspects the URL’s hostname and blocks requests to private infrastructure:
Private IPv4 Ranges
0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16IPv6 Addresses
Loopback (
::1), unique local (fc00::/7), link-local (fe80::/10), IPv4-mapped (::ffff:127.0.0.1)Cloud Metadata
169.254.169.254 (AWS/GCP), metadata.google.internal, metadata.googleObfuscation Techniques
Bare integer IPs (
http://2130706433), octal notation (0177.0.0.1), hex notation (0x7f.0.0.1)http: and https: only, blocking file:, ftp:, gopher:, and other protocol handlers.
IPv4-Mapped IPv6 Detection
A sophisticated attacker might attempt to bypass IPv4 range checks by using IPv4-mapped IPv6 addresses. The guard handles both dotted-quad form and hexadecimal form:Security Headers
Every HTTP response — including WebSocket upgrade responses, health checks, and 404s — includes a comprehensive set of security headers. These are applied unconditionally, not selectively:| Header | Purpose |
|---|---|
Strict-Transport-Security | Forces HTTPS for 1 year, including subdomains |
Content-Security-Policy | Restricts resource loading to same-origin; allows WebSocket connections |
X-Frame-Options: DENY | Prevents clickjacking via iframes |
X-Content-Type-Options: nosniff | Prevents MIME type sniffing |
Referrer-Policy | Sends origin only on cross-origin requests |
Permissions-Policy | Denies access to camera, microphone, and geolocation APIs |
X-DNS-Prefetch-Control: off | Disables DNS prefetching to prevent information leakage |
X-XSS-Protection: 0 | Disables the legacy XSS Auditor, whose known bypass quirks can introduce vulnerabilities worse than the ones it ostensibly prevents |
Error Sanitization
All error messages sent to clients pass throughsanitizeErrorMessage, which applies three transformations in sequence. The goal is to be helpful without being confessional — the client learns what went wrong, but not how the system is built:
Strip Stack Traces
Removes lines matching the pattern
\n\s*at\s+... — standard V8 stack trace lines that would expose internal file paths and module structure.Rate Limiting
Per-Connection Rate Limiter
Every WebSocket connection receives its own sliding-window rate limiter. The window is local to the connection — there is no cross-connection coordination, and the limiter is removed from the tracking map when the WebSocket closes:- Limit: 60 messages per 10-second window
- Enforcement: Checked before message parsing; exceeding the limit returns an
errorevent with codeRATE_LIMITED - Cleanup: Removed when the WebSocket closes
Authentication Rate Limiter
A separate, IP-based rate limiter protects the authentication endpoint from brute-force attacks:| Parameter | Value |
|---|---|
| Max attempts | 10 per IP |
| Window | 60 seconds |
| Lockout duration | 5 minutes |
| Max tracked IPs | 10,000 |
Input Validation
Schema Validation
Every incoming WebSocket message is validated against Effect Schema definitions before processing. TheClientMessage schema is a union of all message types, each with its own field requirements. Messages that do not match any variant are rejected immediately — they never reach the message router:
Message Size Limit
Raw messages exceeding 1 MB are rejected before JSON parsing, preventing denial-of-service via oversized payloads. The check operates on the raw byte length, not the parsed structure:Session ID Path Traversal Prevention
Session IDs are used to construct file paths for per-session SQLite databases. TheresolveSessionDir function validates that the resolved path stays within the sessions base directory — a traversal attempt like ../../etc/passwd would resolve to a path outside sessionsBaseDir and be rejected:
Tenant ID Validation
Tenant IDs extracted from JWT claims are validated against a strict pattern before use. Only alphanumeric characters, hyphens, and underscores are permitted. This prevents directory traversal and SQL injection through the tenant ID:Transport Security
WebSocket Upgrade Validation
The WebSocket upgrade path (/ws) validates the Origin header before upgrading the connection. Non-browser clients (which do not send Origin) are allowed through, but browser-based connections from unauthorized origins are rejected with a 403 response.
Connection Lifecycle
| Parameter | Value |
|---|---|
| Max payload length | 2 MB (maxPayloadLength) |
| Idle timeout | 120 seconds |
| Per-message deflate | Disabled (avoids CRIME-class compression attacks) |
| Heartbeat interval | 30 seconds |
WAL Mode SQLite
All SQLite databases are opened withPRAGMA journal_mode = WAL. WAL mode prevents database corruption from concurrent access — reader and writer workers accessing the same file — and provides crash recovery: incomplete transactions are rolled back automatically on the next open.
synchronous = NORMAL provides a balance between durability and performance: data is safe after a process crash but may be lost on an OS crash or power failure. For a gateway managing ephemeral agent sessions, this tradeoff is appropriate — the Podium coordinator retains the authoritative event log, and session data can be reconstructed from upstream.