If you expose webhook endpoints to the internet, every bot, scanner, and script kiddie on the planet will find them. I needed a way to lock down the webhooks for OpenClaw without bolting security logic onto the gateway itself. The result is Carapace — a lightweight reverse proxy that sits in front of OpenClaw and rejects everything that doesn’t look right.
The Problem
OpenClaw exposes HTTP endpoints that third-party services call into. Putting those endpoints on the open internet without protection is asking for trouble: credential stuffing, oversized payloads meant to exhaust memory, malformed JSON, and plain brute-force attempts.
I wanted a dedicated layer that handles all of this before a request ever reaches the application. Something small, fast, and easy to reason about.
Architecture
The stack runs as three Docker containers behind a single public entry point:
Internet ──► Caddy (TLS, :443) ──► Carapace (:3000) ──► OpenClaw (:18789)
| Service | Role |
|---|---|
| Caddy | TLS termination, security headers, reverse proxy |
| Carapace | Auth, rate limiting, validation, proxying |
| OpenClaw | The actual webhook processing backend |
Only Caddy is exposed. Carapace and OpenClaw talk on an internal Docker network and are never reachable from outside.
What Carapace Does
Every incoming request passes through a strict pipeline:
- Health check —
GET /healthis the only unauthenticated endpoint. - Rate limiting — Sliding-window rate limiter per IP (default: 30 req/min).
- Progressive lockout — 3 failed auth attempts lock the IP for 5 minutes.
- Authentication — Bearer token, HMAC-SHA256 signature, or both. All comparisons are timing-safe.
- Body validation — Content-Length is checked before allocation. The body is stream-read with a hard size cap (64 KB default). JSON is parsed and validated against per-endpoint schemas.
- Proxying — Only requests that survive all checks are forwarded to OpenClaw with proper
X-Forwarded-Forand auth headers.
If any step fails, the request is rejected with a clear status code and the client never touches the backend.
Tech Stack
- Bun as the runtime — fast startup, built-in
Bun.serve(), native TypeScript - TypeScript with strict types across the board
- Caddy for automatic TLS via Let’s Encrypt
- Docker Compose for orchestration
The entire proxy is around 500 lines of TypeScript split across five modules: index.ts, auth.ts, rate-limit.ts, validate.ts, proxy.ts, and logger.ts.
Security Details
A few things I paid extra attention to:
Timing-safe comparisons — Token and HMAC verification use constant-time comparison to prevent timing attacks. This is easy to get wrong; Bun’s CryptoHasher and manual byte-level comparison handle it.
No tokens in query strings — Requests that pass a token via ?token=... are rejected with a 400. Query strings end up in logs, browser history, and referer headers. Tokens belong in headers only.
Structured logging without secrets — Every request is logged as JSON with method, path, IP, status, and latency. Bodies and tokens are never logged.
Docker hardening — Carapace runs as a non-root user in a read-only filesystem with a 64 MB memory limit.
Deployment
Getting it running takes a few minutes:
git clone https://github.com/steffenstein/carapace.git
cd carapace
cp .env.example .env
# Set CARAPACE_TOKEN, OPENCLAW_HOOKS_TOKEN, OPENCLAW_GATEWAY_TOKEN, and DOMAIN
docker compose up -d
Caddy provisions a TLS certificate automatically. For TLS to work you need a domain pointing at your server — I’ve tested it with a cheap Porkbun domain, Cloudflare Tunnel, and DuckDNS. All three work fine.
Configuration
Everything is controlled via environment variables:
| Variable | Default | Purpose |
|---|---|---|
CARAPACE_TOKEN | — | Bearer token for request auth |
CARAPACE_HMAC_SECRET | — | HMAC-SHA256 signing secret |
RATE_LIMIT_MAX | 30 | Max requests per window |
RATE_LIMIT_WINDOW_MS | 60000 | Rate-limit window in ms |
MAX_BODY_SIZE | 65536 | Max request body in bytes |
PROXY_TIMEOUT_MS | 30000 | Upstream timeout in ms |
LOG_LEVEL | info | debug, info, warn, error |
Set CARAPACE_TOKEN for token auth, CARAPACE_HMAC_SECRET for HMAC auth, or both for dual verification.
Was It Worth Building?
Yes. The alternative was scattering auth checks, rate limiting, and validation across OpenClaw itself — mixing infrastructure concerns with application logic. With Carapace sitting in front, OpenClaw can trust that every request it receives has already been authenticated, rate-limited, and validated. The separation is clean and each piece is easy to test on its own.
The source code is on GitHub and the image is on Docker Hub.