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
65 lines
2.4 KiB
JavaScript
65 lines
2.4 KiB
JavaScript
import fs from 'fs';
|
|
import crypto from 'crypto';
|
|
|
|
var ALLOWLIST_PATH = '/etc/nginx/gitea-repo-allowlist.json';
|
|
var SECRET_PATH = '/etc/nginx/gitea-webhook-secret';
|
|
|
|
function getSecret() {
|
|
try { return fs.readFileSync(SECRET_PATH).toString().trim(); }
|
|
catch (e) { return null; }
|
|
}
|
|
|
|
function loadAllowlist() {
|
|
try { return JSON.parse(fs.readFileSync(ALLOWLIST_PATH).toString()); }
|
|
catch (e) { return null; }
|
|
}
|
|
|
|
function isRepoAllowed(repoFullName, allowlist) {
|
|
if (!allowlist || !repoFullName) return false;
|
|
if ((allowlist.repos || []).indexOf(repoFullName) !== -1) return true;
|
|
var owners = allowlist.trusted_owners || [];
|
|
for (var i = 0; i < owners.length; i++) {
|
|
if (repoFullName.startsWith(owners[i] + '/')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function constantTimeEqual(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
var result = 0;
|
|
for (var i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
return result === 0;
|
|
}
|
|
|
|
async function verifyAndProxy(r) {
|
|
var secret = getSecret();
|
|
if (!secret) { r.error('gitea-hmac: failed to read secret'); r.return(500, 'Config error'); return; }
|
|
|
|
var giteaSig = r.headersIn['X-Gitea-Signature'];
|
|
if (!giteaSig) { r.error('gitea-hmac: missing X-Gitea-Signature'); r.return(403, 'Missing signature'); return; }
|
|
|
|
var body = r.requestText || '';
|
|
var hmac = crypto.createHmac('sha256', secret); hmac.update(body);
|
|
if (!constantTimeEqual(hmac.digest('hex'), giteaSig)) {
|
|
r.error('gitea-hmac: signature mismatch'); r.return(403, 'Invalid signature'); return;
|
|
}
|
|
|
|
var allowlist = loadAllowlist();
|
|
if (!allowlist) { r.error('gitea-hmac: cannot read allowlist'); r.return(403, 'Authorization unavailable'); return; }
|
|
|
|
var repoFullName = '';
|
|
try { repoFullName = (JSON.parse(body).repository || {}).full_name || ''; }
|
|
catch (e) { r.error('gitea-hmac: invalid JSON'); r.return(403, 'Invalid body'); return; }
|
|
|
|
if (!repoFullName) { r.error('gitea-hmac: no repo name'); r.return(403, 'Missing repo'); return; }
|
|
if (!isRepoAllowed(repoFullName, allowlist)) {
|
|
r.error('gitea-hmac: BLOCKED ' + repoFullName); r.return(403, 'Not authorized'); return;
|
|
}
|
|
|
|
var res = await r.subrequest('/hooks/gitea-upstream', { method: r.method, body: body });
|
|
var ct = res.headersOut['Content-Type']; if (ct) r.headersOut['Content-Type'] = ct;
|
|
r.return(res.status, res.responseBody);
|
|
}
|
|
|
|
export default { verifyAndProxy };
|