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
78 lines
2.3 KiB
Bash
Executable File
78 lines
2.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# ntfy-blocked-pickup.sh - Scans nginx error log for blocked webhook attempts
|
|
# and sends ntfy.sh notifications for each new occurrence.
|
|
#
|
|
# Designed to run as a cron job every 60 seconds:
|
|
# * * * * * /opt/webhook-security/scripts/ntfy-blocked-pickup.sh
|
|
#
|
|
# State file tracks the last-seen log position to avoid duplicate alerts.
|
|
# ------------------------------------------------------------------
|
|
|
|
set -euo pipefail
|
|
|
|
# ========================= CONFIGURATION =========================
|
|
# Customize these values for your environment
|
|
|
|
ERROR_LOG="/var/log/nginx/error.log"
|
|
STATE_FILE="/var/lib/webhook-security/ntfy-pickup-state"
|
|
NTFY_TOPIC="@@NTFY_TOPIC@@"
|
|
PATTERN="gitea-hmac: BLOCKED webhook from unauthorized repo:"
|
|
|
|
# =================================================================
|
|
|
|
# Ensure state directory exists
|
|
mkdir -p "$(dirname "$STATE_FILE")"
|
|
|
|
# Read last processed byte offset (0 if first run)
|
|
if [ -f "$STATE_FILE" ]; then
|
|
LAST_OFFSET=$(cat "$STATE_FILE")
|
|
else
|
|
LAST_OFFSET=0
|
|
fi
|
|
|
|
# Get current file size
|
|
if [ ! -f "$ERROR_LOG" ]; then
|
|
echo "No error log found at $ERROR_LOG"
|
|
exit 0
|
|
fi
|
|
|
|
CURRENT_SIZE=$(stat -c%s "$ERROR_LOG" 2>/dev/null || stat -f%z "$ERROR_LOG" 2>/dev/null)
|
|
|
|
# Handle log rotation (file shrank)
|
|
if [ "$CURRENT_SIZE" -lt "$LAST_OFFSET" ]; then
|
|
LAST_OFFSET=0
|
|
fi
|
|
|
|
# No new data
|
|
if [ "$CURRENT_SIZE" -eq "$LAST_OFFSET" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Extract new lines and filter for blocked webhook entries
|
|
NEW_BLOCKS=$(tail -c +"$((LAST_OFFSET + 1))" "$ERROR_LOG" | grep -F "$PATTERN" || true)
|
|
|
|
if [ -n "$NEW_BLOCKS" ]; then
|
|
# Count blocked attempts
|
|
COUNT=$(echo "$NEW_BLOCKS" | wc -l)
|
|
|
|
# Extract unique repo names
|
|
REPOS=$(echo "$NEW_BLOCKS" | grep -oP 'unauthorized repo: \K\S+' | sort -u | tr '\n' ', ' | sed 's/,$//')
|
|
|
|
# Build notification message
|
|
MSG="BLOCKED: ${COUNT} webhook(s) from unauthorized repo(s): ${REPOS}"
|
|
|
|
# Send ntfy notification
|
|
curl -sf \
|
|
-H "Title: Gitea Webhook Blocked" \
|
|
-H "Priority: urgent" \
|
|
-H "Content-Type: text/plain" \
|
|
-d "$MSG" \
|
|
"$NTFY_TOPIC" > /dev/null 2>&1 || true
|
|
|
|
# Also log to syslog
|
|
logger -t webhook-security "ntfy-blocked-pickup: $MSG"
|
|
fi
|
|
|
|
# Update state file with current position
|
|
echo "$CURRENT_SIZE" > "$STATE_FILE"
|