Properties
category: spec
tags: [security, caddy, rate-limiting, headers, plan]
last_updated: 2026-03-17
confidence: high

Rate Limiting and Security Headers Plan

Both handled in Caddy on proxy-1. Two phases: security headers (immediate, zero risk) and rate limiting (requires xcaddy rebuild for the rate_limit module).

Phase 1: Security Headers (no module change)

Add to both robot.wtf and *.robot.wtf site blocks:

caddyfile
header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "SAMEORIGIN"
    -Server
}

Omitting CSP (requires script audit), Referrer-Policy, Permissions-Policy (low priority). No preload on HSTS (permanent, hard to undo).

Phase 2: Rate Limiting (requires xcaddy)

The problem

Standard Caddy Debian package has no rate limiting module. Need to build with xcaddy build --with github.com/mholt/caddy-ratelimit.

Rate limits by endpoint

Endpoint Limit Rationale
POST /auth/login 5/min per IP Triggers external ATProto resolution
POST /auth/signup 3/min per IP Creates user records
GET /auth/* (general) 60/min per IP Mostly redirects/JSON
MCP OAuth (/authorize*, /token, /register) 5/min per IP Consent logic
MCP tool calls (/mcp/*) 60/min per IP AI agent bursts (30-60 calls/conversation)
Wiki writes (POST */save, */attachments*) 20/min per IP Content modification
Wiki reads No explicit limit (200/min if needed) Low risk, cacheable

All per-IP using {remote_host}. User-level limits deferred to application layer.

MCP considerations

  • SSE connections are long-lived — don't rate limit /mcp itself (the stream endpoint)
  • Rate limit /mcp/* (tool call messages) at 60/min — one AI conversation can make 30-60 calls
  • MCP OAuth flow gets auth-level limits (5/min)

Implementation sequence

  1. Phase 1 (immediate): Add header blocks to Caddyfile, reload. Verify with curl -I.
  2. Phase 2a: Build xcaddy binary with rate_limit module, add to proxy Ansible repo.
  3. Phase 2b: Add rate_limit directives to Caddyfile template, deploy to proxy-1.
  4. Validate: caddy list-modules confirms module; curl loops verify 429s after threshold.

Rollback

  • Headers: remove block, reload. Strictly additive, minimal risk.
  • Rate limits: remove directives or raise events values, reload. No restart needed.
  • Monitor with journalctl -u caddy -f | grep '" 429 ' after deployment.

Important notes

  • Caddy config is on proxy-1, managed by a separate Ansible repo (not robot.wtf)
  • proxy-1 is the outermost proxy — {remote_host} is the real client IP
  • rate_limit zone names must be globally unique across the Caddyfile