morphit/apps/web/scripts/bunkerweb-cidr-cross-reference-smoke.ts

184 lines
7.6 KiB
TypeScript

#!/usr/bin/env tsx
/**
* bunkerweb-cidr-cross-reference-smoke.
*
* Part 122 cp61 STRUCTURAL DEFENSE (LL #64 / O-14).
*
* Closes the cp61-D1 class: the canonical BunkerWeb Docker network
* CIDR is referenced across many operator-facing files, and the
* Ansible default for `morphit_relay_trusted_proxy_ips` MUST match
* what the bunkerweb role's docker-compose actually deploys. If
* any file drifts to a different CIDR, operators get a silently-
* broken trusted-proxy chain → per-IP rate limiting breaks → all
* user signups bucket into the BunkerWeb container's single IP.
*
* Bug history (cp61-D1):
* - Pre-cp61: group_vars/all.yml defaulted
* `morphit_relay_trusted_proxy_ips: "172.18.0.0/16"` (the
* comment claimed "typical user-defined compose CIDR").
* - But the bunkerweb role's docker-compose pins subnet at
* `172.20.0.0/16` (chosen deliberately to avoid Docker
* defaults).
* - An operator running the default Ansible playbook gets
* BunkerWeb on 172.20.0.0/16 but the relay trusts only
* 172.18.0.0/16 → BunkerWeb's X-Forwarded-For is REJECTED,
* relay falls back to peer IP (BunkerWeb's container IP),
* all users bucket into one rate-limit slot.
* - This is exactly the §32 CRITICAL failure mode the cp57
* audit warned about, hidden behind playbook defaults.
* - Fix at cp61: group_vars/all.yml default changed to
* 172.20.0.0/16, with a comment block explaining the
* coupling to the bunkerweb role's docker-compose.
*
* Enforcement model:
* 1. SOURCE OF TRUTH: `ops/bunkerweb/docker-compose.yml`'s
* `bunkerweb_net` subnet line. Whatever CIDR is pinned
* there is canonical.
* 2. The Ansible bunkerweb role's docker-compose.yml.j2 MUST
* pin the same CIDR (both serve the same network).
* 3. The Ansible group_vars default for
* `morphit_relay_trusted_proxy_ips` MUST match the canonical
* CIDR (otherwise the default Ansible playbook deploys with
* broken trusted-proxy chain).
* 4. Documentation files that reference the bunkerweb CIDR
* (operator-facing examples + READMEs + OPERATIONS.md +
* RUN-A-MORPHIT-NODE.md + PRE-LAUNCH-CHECKLIST.md +
* MORPHIT-BRAG-LIST.md) MUST mention the canonical CIDR.
*
* Note: docs that describe Docker's DEFAULT bridge ranges
* (172.17.0.0/16, 172.18.0.0/16, 172.31.0.0/16) for general
* troubleshooting are LEGITIMATE references — they're not
* bunkerweb-canonical claims. This smoke distinguishes by
* looking only at lines that mention "bunkerweb" or
* "bunkerweb_net" or "TRUSTED_PROXY_IPS=" in close proximity
* to a CIDR.
*
* Recurring class scope progression (16 defenses across 14 checkpoints):
* cp48-O1 through cp60-O13 (as listed above)
* cp61-O14: bunkerweb CIDR cross-reference (THIS)
*
* Mutation test verification: M-128 — change
* group_vars/all.yml's trusted_proxy_ips to 172.18.0.0/16
* (the pre-cp61 broken state) fires the smoke with
* "Ansible default trusted_proxy_ips '172.18.0.0/16' does not
* match canonical bunkerweb CIDR '172.20.0.0/16'".
*/
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_ROOT = join(__dirname, '..', '..', '..');
let failed = 0;
let passed = 0;
function pass(name: string): void { console.log(`${name}`); passed++; }
function fail(name: string, detail: string): void {
console.error(`${name}`); console.error(` ${detail}`); failed++;
}
console.log('\n── bunkerweb-cidr-cross-reference smoke (cp61 LL #64 / O-14) ──\n');
/** Read SOURCE OF TRUTH: the bunkerweb canonical docker-compose's subnet line. */
function readCanonicalCidr(): string {
const composeSrc = readFileSync(
join(REPO_ROOT, 'ops/bunkerweb/docker-compose.yml'),
'utf-8'
);
const m = composeSrc.match(/subnet:\s*([\d./]+)/);
if (!m) {
throw new Error('Cannot locate `subnet:` line in ops/bunkerweb/docker-compose.yml');
}
return m[1];
}
const CANONICAL_CIDR = readCanonicalCidr();
console.log(`SOURCE OF TRUTH: ops/bunkerweb/docker-compose.yml subnet = ${CANONICAL_CIDR}\n`);
/** Check the Ansible bunkerweb role's docker-compose template. */
function checkAnsibleBunkerwebRole(): void {
const path = 'ops/ansible/roles/bunkerweb/templates/docker-compose.yml.j2';
const src = readFileSync(join(REPO_ROOT, path), 'utf-8');
const m = src.match(/subnet:\s*([\d./]+)/);
if (!m) {
fail(`Ansible bunkerweb role subnet present`, `No \`subnet:\` line in ${path}`);
return;
}
if (m[1] !== CANONICAL_CIDR) {
fail(
`Ansible bunkerweb role subnet matches canonical`,
`${path} pins subnet '${m[1]}' but canonical is '${CANONICAL_CIDR}'. ` +
`Both must agree — they deploy the same network.`
);
} else {
pass(`Ansible bunkerweb role subnet matches canonical: ${m[1]}`);
}
}
/** Check the Ansible group_vars default for morphit_relay_trusted_proxy_ips. */
function checkAnsibleTrustedProxyDefault(): void {
const path = 'ops/ansible/group_vars/all.yml';
const src = readFileSync(join(REPO_ROOT, path), 'utf-8');
const m = src.match(/morphit_relay_trusted_proxy_ips:\s*["']?([\d./,\s]+?)["']?\s*(?:#|$)/m);
if (!m) {
fail(`Ansible group_vars trusted_proxy_ips default present`, `Variable not found in ${path}`);
return;
}
const defaultVal = m[1].trim();
// Accept comma-separated lists — canonical CIDR must appear as one of them.
const entries = defaultVal.split(',').map((s) => s.trim()).filter(Boolean);
if (!entries.includes(CANONICAL_CIDR)) {
fail(
`Ansible group_vars default trusts bunkerweb CIDR`,
`Default morphit_relay_trusted_proxy_ips '${defaultVal}' does not include canonical bunkerweb CIDR '${CANONICAL_CIDR}'.\n ` +
`§32 CRITICAL: if Ansible deploys bunkerweb on '${CANONICAL_CIDR}' but the relay trusts a different CIDR,\n ` +
`X-Forwarded-For is rejected and per-IP rate limiting silently breaks.\n ` +
`Fix: update group_vars/all.yml to '${CANONICAL_CIDR}' (or change the bunkerweb role's docker-compose subnet to match this default).`
);
} else {
pass(
`Ansible group_vars default trusts canonical bunkerweb CIDR (entries: ${entries.join(', ')})`
);
}
}
/** Check that cross-reference files mention the canonical CIDR.
* These files document the bunkerweb deployment to operators; if they
* reference a different CIDR, operators get confused — and worse, they
* may copy the wrong CIDR into their config. */
const CROSS_REFERENCE_FILES = [
'ops/bunkerweb/README.md',
'ops/bunkerweb/bunkerweb.env.example',
'ops/ansible/roles/bunkerweb/templates/bunkerweb.env.j2',
'docs/OPERATIONS.md',
'docs/PRE-LAUNCH-CHECKLIST.md',
'MORPHIT-BRAG-LIST.md'
];
function checkCrossReference(file: string): void {
const src = readFileSync(join(REPO_ROOT, file), 'utf-8');
if (!src.includes(CANONICAL_CIDR)) {
fail(
`${file} mentions canonical CIDR`,
`File should reference the canonical bunkerweb CIDR '${CANONICAL_CIDR}' somewhere ` +
`(it documents the bunkerweb deployment to operators).`
);
return;
}
pass(`${file} mentions canonical CIDR '${CANONICAL_CIDR}'`);
}
checkAnsibleBunkerwebRole();
checkAnsibleTrustedProxyDefault();
for (const file of CROSS_REFERENCE_FILES) checkCrossReference(file);
const total = passed + failed;
console.log(`\n${passed} passed, ${failed} failed (${total} total)`);
if (failed > 0) {
console.error('\nbunkerweb-cidr-cross-reference smoke FAILED');
console.error('§32 CRITICAL — getting the trusted-proxy CIDR wrong silently breaks per-IP rate limiting.');
process.exit(1);
}
console.log(`✓ all ${total} bunkerweb CIDR cross-reference checks pass`);