# Webhook Security for OpenClaw This document covers the 5-layer Gitea webhook security system that can be installed alongside the OAuth token sync. The security system protects your OpenClaw webhook endpoint from unauthorized requests. Source: [sol/clawgravity-hook-security](https://git.eeqj.de/sol/clawgravity-hook-security) (v2.0) ## Overview When you install OpenClaw, it exposes a `/hooks/gitea` endpoint that receives Gitea webhooks and routes them to your agents. Without protection, anyone who can reach this endpoint can send arbitrary webhook payloads to your agents. This security system adds five independent layers of defense: | Layer | Mechanism | Where | | ----- | ---------------------------------- | --------- | | 1 | IP allowlisting | nginx | | 2 | Rate limiting (10 req/s, burst 20) | nginx | | 3 | Payload size limit (1MB) | nginx | | 4 | HMAC-SHA256 signature verification | nginx njs | | 5 | Per-repository allowlist | nginx njs | Every layer operates independently. A bypass of any single layer does not compromise the endpoint — all layers must pass for a request to be proxied. ## Architecture ``` Gitea Server | v (HTTPS) nginx | +-- Layer 1: IP allowlist (allow Gitea IP, deny all) +-- Layer 2: Rate limit (10 req/s, burst 20) +-- Layer 3: Payload size (1MB max) +-- Layer 4+5: njs HMAC verify + repo allowlist | v (internal proxy, only if all layers pass) OpenClaw Gateway (:3000/hooks/gitea) ``` ## Installed Files After running `./setup.sh` with webhook security enabled: | File | Purpose | | -------------------------------------------------------- | -------------------------------------- | | `/etc/nginx/njs/gitea-hmac-verify.js` | njs module (Layers 4 + 5) | | `/etc/nginx/gitea-webhook-secret` | HMAC shared secret (root:www-data 640) | | `/etc/nginx/gitea-repo-allowlist.json` | Repository allowlist | | `/opt/webhook-security/scripts/rotate-webhook-secret.sh` | Monthly secret rotation | | `/opt/webhook-security/scripts/webhook-audit-alert.sh` | Daily audit summaries | | `/opt/webhook-security/scripts/ntfy-blocked-pickup.sh` | Real-time blocked alerts | | `/usr/local/bin/gitea-approve-repo` | Add repos to allowlist | Template files (reference only, not deployed): | File | Purpose | | -------------------------------------------------------------- | -------------------------------- | | `templates/webhook-security/nginx.conf.example` | http block additions | | `templates/webhook-security/nginx-site.conf.example` | Site config with location blocks | | `templates/webhook-security/gitea-repo-allowlist.json.example` | Allowlist format reference | ## nginx Configuration Two changes are required in your nginx configuration. The setup wizard displays these during installation — this section documents them for reference. ### 1. nginx.conf (http block) Add to the `http {}` block in `/etc/nginx/nginx.conf`: ```nginx http { js_path "/etc/nginx/njs/"; js_import gitea_hmac from gitea-hmac-verify.js; limit_req_zone $binary_remote_addr zone=gitea_webhook:1m rate=10r/s; } ``` ### 2. Site config (location blocks) Add to your site configuration (e.g. `/etc/nginx/sites-enabled/openclaw`): ```nginx # Internal upstream for Gitea webhook (post-HMAC verification) location /hooks/gitea-upstream { internal; proxy_pass http://127.0.0.1:YOUR_OPENCLAW_PORT/hooks/gitea; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Authorization "Bearer YOUR_OPENCLAW_GATEWAY_TOKEN"; proxy_set_header X-Gitea-Event $http_x_gitea_event; proxy_set_header X-Gitea-Delivery $http_x_gitea_delivery; proxy_set_header X-Gitea-Signature $http_x_gitea_signature; proxy_buffering off; } # Gitea webhook — HMAC-SHA256 verified by njs before proxying location = /hooks/gitea { # Layer 1: IP allowlisting allow YOUR_GITEA_SERVER_IP; allow 127.0.0.1; deny all; # Layer 2: Rate limiting limit_req zone=gitea_webhook burst=20 nodelay; # Layer 3: Payload size limit client_body_buffer_size 1m; client_max_body_size 1m; # Layer 4+5: HMAC verification + repo allowlist (njs) js_content gitea_hmac.verifyAndProxy; } ``` After editing, validate and reload: ```bash nginx -t && nginx -s reload ``` ## Managing the Repository Allowlist The allowlist at `/etc/nginx/gitea-repo-allowlist.json` controls which repositories are allowed to trigger your webhook. Changes take effect immediately (no nginx reload needed). ### Add a specific repository ```bash gitea-approve-repo owner/repo-name ``` ### Add all repos from a trusted owner Edit `/etc/nginx/gitea-repo-allowlist.json` directly: ```json { "repos": ["owner/specific-repo"], "trusted_owners": ["trusted-org"] } ``` ### View current allowlist ```bash cat /etc/nginx/gitea-repo-allowlist.json ``` ### Remove a repository Edit `/etc/nginx/gitea-repo-allowlist.json` and remove the entry from the `repos` array. ## Secret Rotation The HMAC secret should be rotated periodically. A rotation script is included that handles the full rotation cycle atomically: 1. Generates a new secret 2. Updates all Gitea webhooks with the new secret (via API) 3. Verifies the updates succeeded 4. Writes the new secret to nginx 5. Reloads nginx 6. Rolls back everything if any step fails ### Manual rotation ```bash # Dry run first (no changes) /opt/webhook-security/scripts/rotate-webhook-secret.sh --dry-run # Actual rotation sudo /opt/webhook-security/scripts/rotate-webhook-secret.sh ``` ### Prerequisites for rotation - `jq` installed - A Gitea admin token file at `/etc/nginx/gitea-admin-token` (create with: `echo "YOUR_GITEA_ADMIN_TOKEN" > /etc/nginx/gitea-admin-token && chmod 600 /etc/nginx/gitea-admin-token`) ### Automated rotation (cron) The setup wizard offers to install a monthly cron job: ``` 0 3 1 * * /opt/webhook-security/scripts/rotate-webhook-secret.sh >> /var/log/webhook-secret-rotation.log 2>&1 ``` After rotation, you do NOT need to update your Gitea webhook settings — the rotation script updates Gitea automatically. ## Monitoring ### Blocked webhook alerts (ntfy) The `ntfy-blocked-pickup.sh` script scans nginx's error log for blocked webhook attempts and sends ntfy.sh push notifications. When run every minute via cron, you get near-real-time alerts when someone attempts to trigger your webhook from an unauthorized repository. To view blocked attempts manually: ```bash grep "gitea-hmac: BLOCKED" /var/log/nginx/error.log ``` ### Daily audit summary (Mattermost) The `webhook-audit-alert.sh` script analyzes OpenClaw's webhook event logs for the previous day and posts a summary to a Mattermost channel. It also creates Gitea issues for anomalies (rejected/untrusted events). Log format expected: JSONL at `/var/lib/openclaw/hooks/logs/webhook-events-YYYY-MM.jsonl` ## Security Properties ### Fail-Closed Every error path denies the request: - Missing secret file → 500 (request rejected before HMAC check) - Missing allowlist file → 403 (all repos blocked) - Corrupt allowlist JSON → 403 (parse failure = blocked) - Missing repo field in payload → 403 - Signature mismatch → 403 There is no misconfiguration that results in an open endpoint. ### No Caching The njs module reads the secret and allowlist from disk on every request. This means: - Secret rotation takes effect immediately (no nginx reload needed) - Allowlist changes take effect immediately - Trade-off: minor disk I/O per request (negligible for webhook volume) ### Constant-Time Comparison HMAC signatures are compared using XOR-based constant-time comparison to prevent timing attacks. See `scripts/webhook-security/gitea-hmac-verify.js`. ## Troubleshooting ### 403 on all webhooks 1. Check nginx error log: `tail -f /var/log/nginx/error.log` 2. Verify secret matches Gitea: `cat /etc/nginx/gitea-webhook-secret` 3. Check allowlist: `cat /etc/nginx/gitea-repo-allowlist.json` 4. Verify njs module loaded: `nginx -T | grep js_` ### 403 "Missing signature" The webhook in Gitea is not configured with a secret. Edit the webhook in Gitea Settings and set the secret to match `/etc/nginx/gitea-webhook-secret`. ### 403 "Invalid signature" The HMAC secret in nginx does not match the one configured in Gitea. Verify they match or run the rotation script. ### 403 "Not authorized" The repository is not in the allowlist. Add it: ```bash gitea-approve-repo owner/repo-name ``` ### nginx -t fails after installation The njs module may not be loaded. Check: ```bash # Verify njs module is installed dpkg -l libnginx-mod-http-js # Install if missing apt-get install -y libnginx-mod-http-js # Verify module file exists ls /usr/lib/nginx/modules/ngx_http_js_module.so ``` ### Permission denied on secret file The secret file must be readable by nginx (www-data group): ```bash chown root:www-data /etc/nginx/gitea-webhook-secret chmod 640 /etc/nginx/gitea-webhook-secret ``` ## Reference - Full test matrix: [docs/SECURITY-AUDIT.md](SECURITY-AUDIT.md) - Template examples: `templates/webhook-security/` - Source code: [sol/clawgravity-hook-security](https://git.eeqj.de/sol/clawgravity-hook-security)