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
310 lines
9.6 KiB
Markdown
310 lines
9.6 KiB
Markdown
# 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)
|