Skip to content

Security

Keryx ships with security defaults that are sensible for development and tightenable for production. Most features are configured via environment variables — no code changes needed to go from development to a hardened production deployment.

Rate Limiting

Rate limiting uses a sliding window algorithm backed by Redis. It's implemented as action middleware, so you can apply it to specific actions or leave it off entirely.

Setup

Add RateLimitMiddleware to any action:

ts
import { RateLimitMiddleware } from "../middleware/rateLimit";

export class UserCreate implements Action {
  name = "user:create";
  middleware = [RateLimitMiddleware];
  // ...
}

The middleware identifies clients by user ID (if authenticated) or IP address (if not), and applies different limits to each:

Config KeyEnv VarDefaultDescription
enabledRATE_LIMIT_ENABLEDtrueMaster toggle (disabled in test)
windowMsRATE_LIMIT_WINDOW_MS60000Sliding window size (ms)
unauthenticatedLimitRATE_LIMIT_UNAUTH_LIMIT20Max requests per window (no session)
authenticatedLimitRATE_LIMIT_AUTH_LIMIT200Max requests per window (logged in)
keyPrefixRATE_LIMIT_KEY_PREFIX"ratelimit"Redis key prefix

When a client exceeds the limit, the action returns a 429 with a message indicating how many seconds until the window resets. Rate limit info is also included in response headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset).

Custom Limits

The checkRateLimit() function is exported for use outside of action middleware — for example, the OAuth registration endpoint uses it with a stricter limit:

ts
import { checkRateLimit } from "../middleware/rateLimit";

const info = await checkRateLimit(`oauth-register:${ip}`, false, {
  limit: config.rateLimit.oauthRegisterLimit, // default: 5
  windowMs: config.rateLimit.oauthRegisterWindowMs, // default: 1 hour
});

Security Headers

Every HTTP response includes security headers by default. Each is configurable via environment variable:

HeaderEnv VarDefault
Content-Security-PolicyWEB_SECURITY_CSPdefault-src 'self'
X-Content-Type-OptionsWEB_SECURITY_CONTENT_TYPE_OPTIONSnosniff
X-Frame-OptionsWEB_SECURITY_FRAME_OPTIONSDENY
Strict-Transport-SecurityWEB_SECURITY_HSTSmax-age=31536000; includeSubDomains
Referrer-PolicyWEB_SECURITY_REFERRER_POLICYstrict-origin-when-cross-origin

These defaults are production-ready. The CSP may need loosening if your backend serves HTML with inline scripts or external resources — adjust via WEB_SECURITY_CSP.

Session cookies are configured with security flags:

Config KeyEnv VarDefaultDescription
cookieHttpOnlySESSION_COOKIE_HTTP_ONLYtruePrevents JavaScript access to the cookie
cookieSecureSESSION_COOKIE_SECUREfalseOnly send cookie over HTTPS
cookieSameSiteSESSION_COOKIE_SAME_SITE"Strict"CSRF protection (Strict, Lax, or None)

For production, set SESSION_COOKIE_SECURE=true so cookies are only transmitted over HTTPS. The SameSite=Strict default prevents CSRF attacks by ensuring cookies aren't sent on cross-origin requests.

CORS

Cross-origin request handling is configured on the web server:

Config KeyEnv VarDefault
allowedOriginsWEB_SERVER_ALLOWED_ORIGINS"*"
allowedMethodsWEB_SERVER_ALLOWED_METHODS"HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
allowedHeadersWEB_SERVER_ALLOWED_HEADERS"Content-Type"

Important: When allowedOrigins is "*" (the default), the server will not send Access-Control-Allow-Credentials: true — this follows the browser spec that forbids wildcard origins with credentials. For production, set WEB_SERVER_ALLOWED_ORIGINS to your specific domain(s) so that credentialed requests (cookies, auth headers) work correctly.

WebSocket Protections

WebSocket connections have several layers of protection:

Origin Validation

Before upgrading an HTTP connection to WebSocket, the server validates the Origin header against config.server.web.allowedOrigins. If the origin doesn't match, the upgrade is rejected. This prevents Cross-Site WebSocket Hijacking (CSWSH) attacks.

Message Limits

Config KeyEnv VarDefaultDescription
websocketMaxPayloadSizeWS_MAX_PAYLOAD_SIZE65536Max message size in bytes (64 KB)
websocketMaxMessagesPerSecondWS_MAX_MESSAGES_PER_SECOND20Per-connection rate limit
websocketMaxSubscriptionsWS_MAX_SUBSCRIPTIONS100Max channel subscriptions per conn

Messages exceeding the payload size are rejected. Clients sending more than the per-second limit are disconnected. These protect against resource exhaustion from misbehaving or malicious clients.

Channel Validation

  • Channel names must match the pattern /^[a-zA-Z0-9:._-]{1,200}$/ — alphanumeric characters plus :, ., _, -, max 200 characters
  • Undefined channels are rejected — if no registered channel matches the requested name, the subscription is denied with a CHANNEL_NOT_FOUND error

OAuth Security

The MCP server's OAuth 2.1 implementation includes several hardening measures:

Redirect URI Validation

When clients register via /oauth/register, redirect URIs are validated:

  • Must be a valid URL
  • Must not contain a fragment (#)
  • Must not contain userinfo (username/password in the URL)
  • Must use HTTPS for non-localhost URIs

When exchanging authorization codes, the redirect URI must match the registered URI exactly (origin + pathname comparison).

Registration Rate Limiting

OAuth client registration (POST /oauth/register) has a separate, stricter rate limit to prevent abuse:

Config KeyEnv VarDefaultDescription
oauthRegisterLimitRATE_LIMIT_OAUTH_REGISTER_LIMIT5Max registrations per window
oauthRegisterWindowMsRATE_LIMIT_OAUTH_REGISTER_WINDOW_MS3600000Window size (1 hour)

Error Stack Traces

By default, error responses include stack traces in development but omit them in production:

Config KeyEnv VarDefault
Web serverWEB_SERVER_INCLUDE_STACK_IN_ERRORStrue (dev), false (prod)
CLICLI_INCLUDE_STACK_IN_ERRORStrue

The web server default is based on NODE_ENV — when NODE_ENV=production, stack traces are automatically hidden from HTTP responses to avoid leaking internal implementation details.

Static File Path Traversal

Static file serving validates requested paths to prevent directory traversal attacks. Requests containing .. segments that would escape the configured static files directory are rejected with a 403.

Production Checklist

When deploying to production, review these environment variables:

bash
# Cookie security — require HTTPS
SESSION_COOKIE_SECURE=true

# CORS — restrict to your domain
WEB_SERVER_ALLOWED_ORIGINS=https://yourapp.com

# Rate limiting — tune for your traffic
RATE_LIMIT_ENABLED=true
RATE_LIMIT_UNAUTH_LIMIT=20
RATE_LIMIT_AUTH_LIMIT=200

# Error responses — hide internals
NODE_ENV=production
# (stack traces auto-disabled when NODE_ENV=production)

# Security headers — defaults are good, customize CSP if needed
WEB_SECURITY_CSP="default-src 'self'; script-src 'self'"

Released under the MIT License.