Integrates the 5-layer Gitea webhook security system from sol/clawgravity-hook-security (v2.0) into the setup wizard. ## What's added ### New files (from clawgravity-hook-security v2.0) - scripts/webhook-security/gitea-hmac-verify.js -- njs HMAC-SHA256 module - scripts/webhook-security/gitea-approve-repo -- allowlist helper - scripts/webhook-security/rotate-webhook-secret.sh -- monthly secret rotation (templated) - scripts/webhook-security/webhook-audit-alert.sh -- daily audit summaries (templated) - scripts/webhook-security/ntfy-blocked-pickup.sh -- blocked webhook alerts (templated) - templates/webhook-security/nginx-site.conf.example - templates/webhook-security/nginx.conf.example - templates/webhook-security/gitea-repo-allowlist.json.example - docs/WEBHOOK-SECURITY.md -- full documentation - docs/SECURITY-AUDIT.md -- 35-case test matrix - tests/test-webhook-security.sh -- 48 offline tests ### Modified files - setup.sh: Step 11 (webhook security wizard with 6 sub-sections) - scripts/uninstall.sh: webhook security cleanup section - README.md: Webhook Security section after Quick Start - Makefile: test target now runs test-webhook-security.sh - .secret-scan-allowlist: allowlist docs/SECURITY-AUDIT.md (test fixture) ## Security layers 1. IP allowlisting (nginx) 2. Rate limiting 10 req/s burst 20 (nginx) 3. Payload size 1MB (nginx) 4. HMAC-SHA256 signature verification (njs) 5. Per-repository allowlist (njs) ## make check - prettier: PASS - secret-scan: PASS - tests: 48/48 PASS Closes #2
9.6 KiB
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 (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:
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):
# 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:
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
gitea-approve-repo owner/repo-name
Add all repos from a trusted owner
Edit /etc/nginx/gitea-repo-allowlist.json directly:
{
"repos": ["owner/specific-repo"],
"trusted_owners": ["trusted-org"]
}
View current allowlist
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:
- Generates a new secret
- Updates all Gitea webhooks with the new secret (via API)
- Verifies the updates succeeded
- Writes the new secret to nginx
- Reloads nginx
- Rolls back everything if any step fails
Manual rotation
# 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
jqinstalled- 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:
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
- Check nginx error log:
tail -f /var/log/nginx/error.log - Verify secret matches Gitea:
cat /etc/nginx/gitea-webhook-secret - Check allowlist:
cat /etc/nginx/gitea-repo-allowlist.json - 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:
gitea-approve-repo owner/repo-name
nginx -t fails after installation
The njs module may not be loaded. Check:
# 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):
chown root:www-data /etc/nginx/gitea-webhook-secret
chmod 640 /etc/nginx/gitea-webhook-secret
Reference
- Full test matrix: docs/SECURITY-AUDIT.md
- Template examples:
templates/webhook-security/ - Source code: sol/clawgravity-hook-security