Carapace – A Hardened Reverse Proxy for Webhooks

How I built a security layer in Bun and TypeScript that validates, rate-limits, and forwards only safe requests to my webhook backend.

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)
ServiceRole
CaddyTLS termination, security headers, reverse proxy
CarapaceAuth, rate limiting, validation, proxying
OpenClawThe 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:

  1. Health checkGET /health is the only unauthenticated endpoint.
  2. Rate limiting — Sliding-window rate limiter per IP (default: 30 req/min).
  3. Progressive lockout — 3 failed auth attempts lock the IP for 5 minutes.
  4. Authentication — Bearer token, HMAC-SHA256 signature, or both. All comparisons are timing-safe.
  5. 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.
  6. Proxying — Only requests that survive all checks are forwarded to OpenClaw with proper X-Forwarded-For and 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:

VariableDefaultPurpose
CARAPACE_TOKENBearer token for request auth
CARAPACE_HMAC_SECRETHMAC-SHA256 signing secret
RATE_LIMIT_MAX30Max requests per window
RATE_LIMIT_WINDOW_MS60000Rate-limit window in ms
MAX_BODY_SIZE65536Max request body in bytes
PROXY_TIMEOUT_MS30000Upstream timeout in ms
LOG_LEVELinfodebug, 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.