feat: merge Gitea webhook security into setup wizard (issue #2)

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
This commit is contained in:
sol
2026-03-01 08:43:02 +00:00
parent 5382ba4999
commit 2db7d7d90a
16 changed files with 1960 additions and 2 deletions

392
setup.sh Executable file → Normal file
View File

@@ -1019,6 +1019,388 @@ else
ERRORS=$((ERRORS + 1))
fi
# ============================================================================
# STEP 11: Gitea Webhook Security (Optional)
# ============================================================================
header "Step 11: Gitea Webhook Security (Optional)"
echo -e " ${DIM}Installs 5-layer HMAC-based security for your Gitea webhook endpoint.${NC}"
echo -e " ${DIM}Provides: IP allowlisting, rate limiting, payload size limits, HMAC${NC}"
echo -e " ${DIM}signature verification, and per-repository allowlisting.${NC}"
echo -e " ${DIM}Source: sol/clawgravity-hook-security (v2.0)${NC}"
echo ""
WEBHOOK_SECURITY_INSTALLED=false
if ! confirm "Set up Gitea webhook security?" "Y"; then
warn "Skipping webhook security setup (can be added later by re-running setup.sh)"
else
# --- 11.1: Prerequisite checks ---
info "Checking webhook security prerequisites..."
WH_MISSING=0
if command -v nginx &>/dev/null; then
success "nginx found: $(nginx -v 2>&1 | head -1)"
else
warn "nginx not found"
echo -e " ${DIM}Install: apt-get install -y nginx${NC}"
WH_MISSING=$((WH_MISSING + 1))
fi
# Check for njs module
NJS_OK=false
if nginx -V 2>&1 | grep -q 'http_js_module\|njs'; then
NJS_OK=true
success "nginx njs module detected"
elif dpkg -l libnginx-mod-http-js 2>/dev/null | grep -q '^ii'; then
NJS_OK=true
success "libnginx-mod-http-js installed"
elif [ -f /usr/lib/nginx/modules/ngx_http_js_module.so ]; then
NJS_OK=true
success "njs module found at /usr/lib/nginx/modules/"
else
warn "nginx njs module not detected"
echo -e " ${DIM}Install: apt-get install -y libnginx-mod-http-js${NC}"
WH_MISSING=$((WH_MISSING + 1))
fi
if command -v jq &>/dev/null; then
success "jq found"
else
warn "jq not found (required for rotation/audit scripts)"
if confirm " Install jq now?" "Y"; then
apt-get install -y jq 2>&1 | tail -3
if command -v jq &>/dev/null; then
success "jq installed"
else
warn "jq installation failed — install manually: apt-get install -y jq"
WH_MISSING=$((WH_MISSING + 1))
fi
else
warn "jq is required for rotation scripts — install later: apt-get install -y jq"
WH_MISSING=$((WH_MISSING + 1))
fi
fi
if command -v openssl &>/dev/null; then
success "openssl found"
else
error "openssl not found — required for secret generation"
WH_MISSING=$((WH_MISSING + 1))
fi
if [ "$WH_MISSING" -gt 0 ]; then
warn "$WH_MISSING prerequisite(s) missing."
if ! confirm " Continue anyway? (some steps may not complete)" "N"; then
warn "Skipping webhook security setup. Install missing deps and re-run."
else
info "Continuing with available tools..."
fi
fi
# --- 11.2: Interactive prompts ---
header "Step 11.2: Webhook Security Configuration"
echo -e " ${DIM}We'll collect the configuration values needed to deploy the security system.${NC}"
echo ""
# Gitea server IP
echo -e " ${BOLD}Gitea Server IP${NC}"
echo -e " ${DIM}The IP address of your Gitea server (for nginx allowlisting, Layer 1).${NC}"
echo -e " ${DIM}Find it with: dig +short YOUR_GITEA_DOMAIN${NC}"
WH_GITEA_IP=$(ask " Gitea server IP" "127.0.0.1")
echo ""
# Webhook HMAC secret
echo -e " ${BOLD}Webhook HMAC Secret${NC}"
echo -e " ${DIM}Used to verify Gitea webhook signatures (Layer 4).${NC}"
echo -e " ${DIM}Must match the secret configured in your Gitea webhook settings.${NC}"
if confirm " Auto-generate a secure secret with openssl?" "Y"; then
WH_SECRET=$(openssl rand -hex 32)
success "Generated secret: ${WH_SECRET:0:8}...${WH_SECRET: -8} (64 hex chars)"
echo -e " ${YELLOW}Important: After setup, update your Gitea webhook secret to this value.${NC}"
echo -e " ${DIM}Gitea Settings -> Webhooks -> [your hook] -> Secret${NC}"
else
WH_SECRET=$(ask " Enter webhook secret" "")
if [ -z "$WH_SECRET" ]; then
WH_SECRET=$(openssl rand -hex 32)
warn "No secret provided — auto-generated: ${WH_SECRET:0:8}..."
fi
fi
echo ""
# Trusted owners
echo -e " ${BOLD}Trusted Gitea Owners${NC}"
echo -e " ${DIM}All repositories from these owners are allowed (Layer 5).${NC}"
echo -e " ${DIM}Space-separated list of Gitea usernames/org names.${NC}"
echo -e " ${DIM}Individual repos can be added later with: gitea-approve-repo owner/repo${NC}"
WH_TRUSTED_OWNERS=$(ask " Trusted owners (space-separated)" "")
echo ""
# ntfy topic (optional)
echo -e " ${BOLD}ntfy Alert Topic (optional)${NC}"
echo -e " ${DIM}Receive instant ntfy.sh alerts when blocked webhooks are detected.${NC}"
echo -e " ${DIM}Leave blank to skip notifications.${NC}"
WH_NTFY_TOPIC=$(ask " ntfy topic URL (e.g. https://ntfy.sh/my-topic)" "")
echo ""
# OpenClaw port
echo -e " ${BOLD}OpenClaw Gateway Port${NC}"
echo -e " ${DIM}The port your OpenClaw gateway listens on (for nginx proxy_pass).${NC}"
WH_OPENCLAW_PORT=$(ask " OpenClaw port" "3000")
echo ""
# Gitea API for rotation script
echo -e " ${BOLD}Gitea Instance URL${NC}"
echo -e " ${DIM}Base URL of your Gitea instance (for secret rotation script).${NC}"
WH_GITEA_INSTANCE=$(ask " Gitea URL" "https://git.example.com")
echo ""
# Webhook URL match pattern
echo -e " ${BOLD}Webhook URL Match Pattern${NC}"
echo -e " ${DIM}Pattern to identify your OpenClaw webhook hooks in Gitea (for rotation).${NC}"
WH_WEBHOOK_URL_MATCH=$(ask " Webhook URL pattern (e.g. yourdomain.com/hooks/gitea)" "")
echo ""
# Scan owners for rotation
echo -e " ${BOLD}Gitea Owner(s) to Scan for Rotation${NC}"
echo -e " ${DIM}Space-separated list of Gitea owners whose repos will have webhooks rotated.${NC}"
WH_SCAN_OWNERS=$(ask " Owners to scan" "${WH_TRUSTED_OWNERS:-your-org}")
echo ""
# Mattermost config for audit (optional)
echo -e " ${BOLD}Mattermost Audit Integration (optional)${NC}"
echo -e " ${DIM}Daily audit summaries posted to a Mattermost channel.${NC}"
WH_MATTERMOST_URL=$(ask " Mattermost URL (leave blank to skip)" "")
WH_MATTERMOST_CHANNEL=""
WH_MATTERMOST_GITEA_REPO=""
if [ -n "$WH_MATTERMOST_URL" ]; then
WH_MATTERMOST_CHANNEL=$(ask " Mattermost channel ID" "")
WH_MATTERMOST_GITEA_REPO=$(ask " Gitea repo for audit anomaly issues (e.g. myorg/webhook-security)" "")
fi
echo ""
# --- 11.3: Deploy files ---
header "Step 11.3: Deploying Webhook Security Files"
WH_INSTALL_DIR="/opt/webhook-security"
WH_NJS_DIR="/etc/nginx/njs"
WH_SECRET_FILE="/etc/nginx/gitea-webhook-secret"
WH_ALLOWLIST_FILE="/etc/nginx/gitea-repo-allowlist.json"
# Create directories
mkdir -p "$WH_INSTALL_DIR/scripts"
mkdir -p "$WH_NJS_DIR"
success "Created $WH_INSTALL_DIR/scripts"
success "Created $WH_NJS_DIR"
# Copy njs HMAC module
cp "$SCRIPT_DIR/scripts/webhook-security/gitea-hmac-verify.js" "$WH_NJS_DIR/"
chmod 644 "$WH_NJS_DIR/gitea-hmac-verify.js"
success "Installed njs module: $WH_NJS_DIR/gitea-hmac-verify.js"
# Write secret file
printf '%s' "$WH_SECRET" > "$WH_SECRET_FILE"
chown root:www-data "$WH_SECRET_FILE" 2>/dev/null || chown root:root "$WH_SECRET_FILE"
chmod 640 "$WH_SECRET_FILE"
success "Installed secret: $WH_SECRET_FILE (permissions: root:www-data 640)"
# Build allowlist JSON from trusted owners
WH_TRUSTED_OWNERS_JSON="[]"
if [ -n "$WH_TRUSTED_OWNERS" ]; then
WH_TRUSTED_OWNERS_JSON=$(python3 -c "
import json, sys
owners = '$WH_TRUSTED_OWNERS'.split()
print(json.dumps(owners))
")
fi
python3 -c "
import json
data = {
'_comment': 'Gitea webhook repo allowlist. Edits take effect immediately (no nginx reload needed).',
'repos': [],
'trusted_owners': $WH_TRUSTED_OWNERS_JSON
}
with open('$WH_ALLOWLIST_FILE', 'w') as f:
json.dump(data, f, indent=4)
f.write('\n')
"
chmod 644 "$WH_ALLOWLIST_FILE"
success "Installed allowlist: $WH_ALLOWLIST_FILE"
info " Trusted owners: ${WH_TRUSTED_OWNERS:-none configured yet}"
info " Add repos later with: gitea-approve-repo owner/repo"
# Install templated scripts with substitutions
for script in rotate-webhook-secret.sh webhook-audit-alert.sh ntfy-blocked-pickup.sh; do
SED_CMD=(sed
-e "s|@@GITEA_API@@|$WH_GITEA_INSTANCE|g"
-e "s|@@WEBHOOK_URL_MATCH@@|$WH_WEBHOOK_URL_MATCH|g"
-e "s|@@SCAN_OWNERS@@|$WH_SCAN_OWNERS|g"
-e "s|@@NTFY_TOPIC@@|${WH_NTFY_TOPIC:-https://ntfy.sh/YOUR_NTFY_TOPIC}|g"
-e "s|@@MATTERMOST_URL@@|${WH_MATTERMOST_URL:-YOUR_MATTERMOST_URL}|g"
-e "s|@@MATTERMOST_CHANNEL_ID@@|${WH_MATTERMOST_CHANNEL:-YOUR_MATTERMOST_CHANNEL_ID}|g"
-e "s|@@GITEA_API_BASE@@|$WH_GITEA_INSTANCE|g"
-e "s|@@GITEA_REPO@@|${WH_MATTERMOST_GITEA_REPO:-YOUR_GITEA_REPO}|g"
)
"${SED_CMD[@]}" "$SCRIPT_DIR/scripts/webhook-security/$script" \
> "$WH_INSTALL_DIR/scripts/$script"
chmod 755 "$WH_INSTALL_DIR/scripts/$script"
success "Installed $WH_INSTALL_DIR/scripts/$script"
done
# Task 11.4: Install gitea-approve-repo to /usr/local/bin/
cp "$SCRIPT_DIR/scripts/webhook-security/gitea-approve-repo" /usr/local/bin/gitea-approve-repo
chmod 755 /usr/local/bin/gitea-approve-repo
success "Installed /usr/local/bin/gitea-approve-repo"
# --- 11.4: nginx config guidance ---
header "Step 11.4: nginx Configuration Guidance"
echo -e " ${BOLD}The security system requires two nginx configuration changes:${NC}"
echo ""
echo -e " ${BOLD}1. In /etc/nginx/nginx.conf (inside the http {} block):${NC}"
echo -e " ${DIM}─────────────────────────────────────────────────────────${NC}"
cat "$SCRIPT_DIR/templates/webhook-security/nginx.conf.example"
echo -e " ${DIM}─────────────────────────────────────────────────────────${NC}"
echo ""
echo -e " ${BOLD}2. In your site config (e.g. /etc/nginx/sites-enabled/openclaw):${NC}"
echo -e " ${DIM}The location blocks for /hooks/gitea are in:${NC}"
echo -e " ${DIM}$SCRIPT_DIR/templates/webhook-security/nginx-site.conf.example${NC}"
echo ""
echo -e " ${DIM}Replace YOUR_DOMAIN, YOUR_GITEA_SERVER_IP, YOUR_OPENCLAW_PORT,${NC}"
echo -e " ${DIM}and YOUR_OPENCLAW_GATEWAY_TOKEN with your actual values.${NC}"
echo ""
if confirm " Display full site config example now?" "N"; then
echo ""
echo -e "${DIM}"
cat "$SCRIPT_DIR/templates/webhook-security/nginx-site.conf.example" | \
sed -e "s|YOUR_GITEA_SERVER_IP|$WH_GITEA_IP|g" \
-e "s|YOUR_OPENCLAW_PORT|$WH_OPENCLAW_PORT|g"
echo -e "${NC}"
fi
# --- 11.5: Optional cron setup ---
header "Step 11.5: Cron Jobs (Optional)"
echo -e " ${DIM}Three automated tasks can be set up via cron:${NC}"
echo ""
echo " 1. Secret rotation (monthly, 1st of month at 3am UTC)"
echo " Command: $WH_INSTALL_DIR/scripts/rotate-webhook-secret.sh"
echo ""
echo " 2. Blocked webhook alerts (every minute, scans nginx error.log)"
echo " Command: $WH_INSTALL_DIR/scripts/ntfy-blocked-pickup.sh"
echo ""
echo " 3. Daily audit summary (5am UTC)"
echo " Command: $WH_INSTALL_DIR/scripts/webhook-audit-alert.sh"
echo ""
CRON_ADDED=0
if confirm " Install monthly secret rotation cron job?" "Y"; then
CRON_LINE="0 3 1 * * $WH_INSTALL_DIR/scripts/rotate-webhook-secret.sh >> /var/log/webhook-secret-rotation.log 2>&1"
(crontab -l 2>/dev/null; echo "$CRON_LINE") | sort -u | crontab -
success "Added rotation cron: 0 3 1 * * ..."
CRON_ADDED=$((CRON_ADDED + 1))
fi
if [ -n "$WH_NTFY_TOPIC" ] && confirm " Install blocked-webhook ntfy alerts cron (every minute)?" "Y"; then
CRON_LINE="* * * * * $WH_INSTALL_DIR/scripts/ntfy-blocked-pickup.sh"
(crontab -l 2>/dev/null; echo "$CRON_LINE") | sort -u | crontab -
success "Added ntfy-blocked-pickup cron: * * * * * ..."
CRON_ADDED=$((CRON_ADDED + 1))
fi
if [ -n "$WH_MATTERMOST_URL" ] && confirm " Install daily audit summary cron (5am UTC)?" "Y"; then
CRON_LINE="5 0 * * * MATTERMOST_BOT_TOKEN=YOUR_BOT_TOKEN $WH_INSTALL_DIR/scripts/webhook-audit-alert.sh >> /var/log/webhook-audit.log 2>&1"
(crontab -l 2>/dev/null; echo "$CRON_LINE") | sort -u | crontab -
success "Added audit cron: 5 0 * * * ..."
warn "Edit the cron entry to set MATTERMOST_BOT_TOKEN: crontab -e"
CRON_ADDED=$((CRON_ADDED + 1))
fi
if [ "$CRON_ADDED" -eq 0 ]; then
info "No cron jobs added. You can add them manually — see docs/WEBHOOK-SECURITY.md"
fi
# --- 11.6: Verification ---
header "Step 11.6: Webhook Security Verification"
WH_ERRORS=0
# Check njs module file
if [ -f "$WH_NJS_DIR/gitea-hmac-verify.js" ]; then
success "njs module installed: $WH_NJS_DIR/gitea-hmac-verify.js"
else
error "njs module NOT found: $WH_NJS_DIR/gitea-hmac-verify.js"
WH_ERRORS=$((WH_ERRORS + 1))
fi
# Check secret file
if [ -f "$WH_SECRET_FILE" ]; then
SECRET_LEN=$(wc -c < "$WH_SECRET_FILE")
SECRET_PERMS=$(stat -c '%a' "$WH_SECRET_FILE" 2>/dev/null || stat -f '%Mp%Lp' "$WH_SECRET_FILE" 2>/dev/null)
if [ "$SECRET_LEN" -ge 32 ]; then
success "Secret file: $WH_SECRET_FILE (${SECRET_LEN} chars, perms: $SECRET_PERMS)"
else
error "Secret file too short (${SECRET_LEN} chars)"
WH_ERRORS=$((WH_ERRORS + 1))
fi
else
error "Secret file NOT found: $WH_SECRET_FILE"
WH_ERRORS=$((WH_ERRORS + 1))
fi
# Check allowlist
if [ -f "$WH_ALLOWLIST_FILE" ]; then
if python3 -c "import json; json.load(open('$WH_ALLOWLIST_FILE'))" 2>/dev/null; then
success "Allowlist file: $WH_ALLOWLIST_FILE (valid JSON)"
else
error "Allowlist file has invalid JSON: $WH_ALLOWLIST_FILE"
WH_ERRORS=$((WH_ERRORS + 1))
fi
else
error "Allowlist file NOT found: $WH_ALLOWLIST_FILE"
WH_ERRORS=$((WH_ERRORS + 1))
fi
# Check gitea-approve-repo
if [ -x /usr/local/bin/gitea-approve-repo ]; then
success "gitea-approve-repo: /usr/local/bin/gitea-approve-repo (executable)"
else
error "gitea-approve-repo not found/executable"
WH_ERRORS=$((WH_ERRORS + 1))
fi
# nginx config test (if nginx is available and njs module found)
if command -v nginx &>/dev/null && $NJS_OK; then
if nginx -t 2>/dev/null; then
success "nginx -t: config is valid"
else
warn "nginx -t failed — config changes may be needed (see Step 11.4 guidance above)"
fi
fi
if [ "$WH_ERRORS" -eq 0 ]; then
success "Webhook security installed successfully"
WEBHOOK_SECURITY_INSTALLED=true
else
warn "$WH_ERRORS webhook security error(s) — review output above"
fi
echo ""
info "Reference: $SCRIPT_DIR/docs/WEBHOOK-SECURITY.md for full documentation"
info "Security audit: $SCRIPT_DIR/docs/SECURITY-AUDIT.md"
fi
# ============================================================================
# SUMMARY
# ============================================================================
@@ -1049,6 +1431,9 @@ fi
echo " - Anthropic model configured in openclaw.json"
echo " - Auth profiles updated for all agents"
echo " - oauth.json created with fresh token"
if [ "$WEBHOOK_SECURITY_INSTALLED" = true ]; then
echo " - Webhook security: njs module, secret, allowlist, helper scripts"
fi
echo ""
echo -e " ${BOLD}Useful commands:${NC}"
if [ "$USE_INOTIFY" = true ]; then
@@ -1062,8 +1447,13 @@ if [ "$TRIGGER_INSTALLED" = true ]; then
echo " journalctl -u trigger-claude-refresh -n 20 # Trigger logs"
echo " systemctl list-timers trigger-claude-refresh* # Check trigger timer"
fi
if [ "$WEBHOOK_SECURITY_INSTALLED" = true ]; then
echo " gitea-approve-repo owner/repo # Add repo to allowlist"
echo " cat /etc/nginx/gitea-repo-allowlist.json # View allowlist"
echo " /opt/webhook-security/scripts/rotate-webhook-secret.sh --dry-run"
fi
echo " ./scripts/verify.sh # Health check"
echo " ./setup.sh --uninstall # Remove everything"
echo ""
echo -e " ${DIM}Created by ROOH — <project-url>${NC}"
echo ""