Files
maestro/scripts/security/check-egress-boundary.mjs
oss-sync 57702b4f34
CI / build-and-test (push) Canceled after 0s
sync: update from private repo (2336949a)
2026-09-17 11:29:31 +00:00

400 lines
15 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Offline regression check for the server egress boundary artifacts.
*
* This check never applies host firewall/systemd policy and never contacts the
* public network. It validates the shipped templates, exercises an in-process
* exact-host+port proxy policy, and demonstrates the transports that still
* require the OS boundary because HTTP proxy environment variables do not
* contain them.
*/
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import http from 'node:http';
import net from 'node:net';
import dgram from 'node:dgram';
import { Resolver } from 'node:dns/promises';
import { execFile } from 'node:child_process';
const SYSTEMD_FILE = new URL('../../deploy/egress/systemd/maestro-egress.service.example', import.meta.url);
const SQUID_FILE = new URL('../../deploy/egress/squid.conf', import.meta.url);
const ALLOWED_HOST = 'allowed.egress.invalid';
const DENIED_HOST = 'denied.egress.invalid';
const ALLOWED_PORT = 443;
const SECRET = 'EGRESS_SECRET_CANARY_7d7fc02c';
function listen(server, host) {
return new Promise((resolve, reject) => {
server.once('error', reject);
const onReady = () => {
server.off('error', reject);
resolve(server.address());
};
if (typeof server.listen === 'function') server.listen(0, host, onReady);
else server.bind(0, host, onReady);
});
}
function close(server) {
server.closeAllConnections?.();
server.close();
}
function runNode(code, env = {}) {
return new Promise((resolve, reject) => {
execFile(
process.execPath,
['--use-env-proxy', '-e', code],
{
env: { PATH: process.env.PATH || '/usr/bin:/bin', HOME: '/tmp', LANG: 'C.UTF-8', ...env },
encoding: 'utf8',
timeout: 8_000,
maxBuffer: 64 * 1024,
},
(error, stdout, stderr) => {
if (error) {
reject(new Error(`local child probe failed: ${stderr.trim() || error.message}`));
return;
}
resolve(stdout.trim());
},
);
});
}
function rawExchange(host, port, request) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host, port });
let response = '';
const timer = setTimeout(() => socket.destroy(new Error('raw probe timed out')), 3_000);
socket.setEncoding('utf8');
socket.on('connect', () => socket.end(request));
socket.on('data', (chunk) => { response += chunk; });
socket.on('error', reject);
socket.on('close', () => {
clearTimeout(timer);
resolve(response);
});
});
}
function parseTarget(value, fallbackScheme = 'http:') {
try {
const url = value.includes('://') ? new URL(value) : new URL(`${fallbackScheme}//${value}`);
return {
hostname: url.hostname,
port: Number(url.port || (url.protocol === 'https:' ? 443 : 80)),
};
} catch {
return { hostname: '', port: 0 };
}
}
function allowedTarget(target) {
return target.hostname === ALLOWED_HOST && target.port === ALLOWED_PORT;
}
async function validateTemplates() {
const [unit, squid] = await Promise.all([
readFile(SYSTEMD_FILE, 'utf8'),
readFile(SQUID_FILE, 'utf8'),
]);
assert.match(unit, /^IPAddressDeny=any$/m);
assert.match(unit, /^IPAddressAllow=127\.0\.0\.2\/32$/m);
assert.match(unit, /^IPAddressAllow=127\.0\.0\.3\/32$/m);
assert.doesNotMatch(unit, /^IPAddressAllow=(?:localhost|127\.0\.0\.0\/8|::1(?:\/128)?)$/m);
assert.match(unit, /ExecStart=\/usr\/bin\/env .*HTTP_PROXY=http:\/\/127\.0\.0\.2:3128 .*HTTPS_PROXY=http:\/\/127\.0\.0\.2:3128 .*NO_PROXY= .*\/usr\/bin\/node --use-env-proxy/m);
assert.doesNotMatch(unit, /NODE_OPTIONS=/);
assert.match(squid, /^http_port 127\.0\.0\.2:3128$/m);
assert.match(squid, /^acl maestro_llm_host dstdomain -n allowed\.egress\.invalid$/m);
assert.match(squid, /^acl maestro_llm_port port 443$/m);
assert.match(squid, /^http_access allow maestro_llm_host maestro_llm_port$/m);
assert.match(squid, /^access_log none$/m);
assert.match(squid, /^cache_store_log none$/m);
const accessRules = squid.split(/\r?\n/).filter((line) => /^http_access\s+/.test(line.trim()));
assert.equal(accessRules.at(-1)?.trim(), 'http_access deny all');
return { systemd: true, squid: true };
}
async function runProxyChecks() {
const events = [];
const proxy = http.createServer((req, res) => {
const selectedTarget = Array.isArray(req.headers['x-egress-target'])
? req.headers['x-egress-target'][0]
: req.headers['x-egress-target'];
const target = parseTarget(selectedTarget || req.url || '');
const allowed = allowedTarget(target);
events.push({ protocol: 'http', decision: allowed ? 'allow' : 'deny', port: target.port });
res.writeHead(allowed ? 200 : 403, { 'content-type': 'text/plain' });
res.end(allowed ? 'allowed' : 'denied');
});
proxy.on('connect', (req, socket) => {
const target = parseTarget(req.url || '', 'http:');
const allowed = allowedTarget(target);
events.push({ protocol: 'connect', decision: allowed ? 'allow' : 'deny', port: target.port });
socket.end(`HTTP/1.1 ${allowed ? '200 Connection Established' : '403 Forbidden'}\r\nConnection: close\r\n\r\n`);
});
proxy.on('upgrade', (req, socket) => {
const target = parseTarget(req.url || '');
const allowed = allowedTarget(target);
events.push({ protocol: 'websocket', decision: allowed ? 'allow' : 'deny', port: target.port });
socket.end(`HTTP/1.1 ${allowed ? '101 Switching Protocols' : '403 Forbidden'}\r\nConnection: close\r\n\r\n`);
});
const address = await listen(proxy, '127.0.0.1');
const proxyUrl = `http://127.0.0.1:${address.port}`;
try {
const fetchAllowed = await fetch(`${proxyUrl}/private?token=${SECRET}`, {
headers: { Authorization: `Bearer ${SECRET}`, 'x-egress-target': `${ALLOWED_HOST}:${ALLOWED_PORT}` },
signal: AbortSignal.timeout(3_000),
});
const fetchDenied = await fetch(`${proxyUrl}/private?token=${SECRET}`, {
headers: { Authorization: `Bearer ${SECRET}`, 'x-egress-target': `${DENIED_HOST}:${ALLOWED_PORT}` },
signal: AbortSignal.timeout(3_000),
});
assert.equal(fetchAllowed.status, 200);
assert.equal(fetchDenied.status, 403);
await Promise.all([fetchAllowed.text(), fetchDenied.text()]);
const connectAllowed = await rawExchange('127.0.0.1', address.port,
`CONNECT ${ALLOWED_HOST}:${ALLOWED_PORT} HTTP/1.1\r\nHost: ${ALLOWED_HOST}:${ALLOWED_PORT}\r\nAuthorization: Bearer ${SECRET}\r\n\r\n`);
const connectDenied = await rawExchange('127.0.0.1', address.port,
`CONNECT ${DENIED_HOST}:${ALLOWED_PORT} HTTP/1.1\r\nHost: ${DENIED_HOST}:${ALLOWED_PORT}\r\n\r\n`);
assert.match(connectAllowed, /^HTTP\/1\.1 200/);
assert.match(connectDenied, /^HTTP\/1\.1 403/);
const wsAllowed = await rawExchange('127.0.0.1', address.port,
`GET ws://${ALLOWED_HOST}:${ALLOWED_PORT}/socket?token=${SECRET} HTTP/1.1\r\nHost: ${ALLOWED_HOST}:${ALLOWED_PORT}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n`);
const wsDenied = await rawExchange('127.0.0.1', address.port,
`GET ws://${DENIED_HOST}:${ALLOWED_PORT}/socket HTTP/1.1\r\nHost: ${DENIED_HOST}:${ALLOWED_PORT}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n`);
assert.match(wsAllowed, /^HTTP\/1\.1 101/);
assert.match(wsDenied, /^HTTP\/1\.1 403/);
const serializedEvents = JSON.stringify(events);
assert.ok(!serializedEvents.includes(SECRET), 'metadata events must not retain request secrets');
assert.equal(events.filter((event) => event.decision === 'allow').length, 3);
assert.equal(events.filter((event) => event.decision === 'deny').length, 3);
return { allowed: 3, denied: 3, secretRetained: false };
} finally {
close(proxy);
}
}
async function reserveTcpPort() {
const server = net.createServer();
const address = await listen(server, '127.0.0.1');
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
return address.port;
}
async function runActualEnvProxyChild(port) {
const events = [];
const proxy = http.createServer((req, res) => {
const target = parseTarget(req.url || '');
const allowed = allowedTarget(target);
events.push({
transport: 'absolute-form',
decision: allowed ? 'allow' : 'deny',
target: `${target.hostname}:${target.port}`,
absoluteUrl: /^http:\/\//.test(req.url || ''),
});
res.writeHead(allowed ? 200 : 403, { 'content-type': 'text/plain', connection: 'close' });
res.end(allowed ? 'allowed' : 'denied');
});
proxy.on('connect', (req, socket) => {
const target = parseTarget(req.url || '', 'http:');
const allowed = allowedTarget(target);
events.push({
transport: 'connect',
decision: allowed ? 'allow' : 'deny',
target: `${target.hostname}:${target.port}`,
absoluteUrl: false,
});
if (!allowed) {
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
return;
}
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
socket.once('data', () => {
socket.end('HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 7\r\nConnection: close\r\n\r\nallowed');
});
});
await new Promise((resolve, reject) => {
proxy.once('error', reject);
proxy.listen(port, '127.0.0.1', () => {
proxy.off('error', reject);
resolve();
});
});
try {
const init = {
headers: { Authorization: `Bearer ${SECRET}` },
signal: AbortSignal.timeout(3_000),
};
const allowedResponse = await fetch(`http://${ALLOWED_HOST}:${ALLOWED_PORT}/private?token=${SECRET}`, init);
const allowedStatus = allowedResponse.status;
await allowedResponse.text();
let deniedStatus = 'error';
try {
const deniedResponse = await fetch(`http://${DENIED_HOST}:${ALLOWED_PORT}/private?token=${SECRET}`, init);
deniedStatus = deniedResponse.status;
await deniedResponse.text();
} catch {}
return { allowedStatus, deniedStatus, events };
} finally {
close(proxy);
}
}
async function runActualEnvProxyCheck() {
const port = await reserveTcpPort();
const proxyUrl = `http://127.0.0.1:${port}`;
const childEnv = {
PATH: process.env.PATH || '/usr/bin:/bin',
HOME: '/tmp',
LANG: 'C.UTF-8',
MAESTRO_EGRESS_PROXY_CHILD: '1',
MAESTRO_EGRESS_PROXY_PORT: String(port),
HTTP_PROXY: proxyUrl,
HTTPS_PROXY: proxyUrl,
NO_PROXY: '',
};
const output = await new Promise((resolve, reject) => {
execFile(
process.execPath,
['--use-env-proxy', fileURLToPath(import.meta.url)],
{ env: childEnv, encoding: 'utf8', timeout: 8_000, maxBuffer: 64 * 1024 },
(error, stdout, stderr) => {
if (error) {
reject(new Error(`env proxy child failed: ${stderr.trim() || error.message}`));
return;
}
resolve(stdout.trim());
},
);
});
assert.ok(!output.includes(SECRET));
const result = JSON.parse(output);
assert.equal(result.allowedStatus, 200);
assert.ok(result.deniedStatus === 403 || result.deniedStatus === 'error');
const allowed = result.events.find((event) => event.decision === 'allow');
const denied = result.events.find((event) => event.decision === 'deny');
assert.equal(allowed?.target, `${ALLOWED_HOST}:${ALLOWED_PORT}`);
assert.equal(denied?.target, `${DENIED_HOST}:${ALLOWED_PORT}`);
if (allowed.transport === 'absolute-form') assert.equal(allowed.absoluteUrl, true);
return {
nodeFlag: '--use-env-proxy',
allowedTransport: allowed.transport,
allowedTarget: allowed.target,
deniedTarget: denied.target,
cleanChildEnvironment: true,
secretRetained: false,
};
}
async function runDirectBypassChecks() {
const hits = { http: 0, raw: 0, childRaw: 0, ipv6: 0, dns: 0 };
const httpServer = http.createServer((req, res) => {
hits.http += 1;
res.end('direct-http');
});
const rawServer = net.createServer((socket) => {
socket.once('data', (data) => {
const kind = data.toString() === 'child' ? 'childRaw' : 'raw';
hits[kind] += 1;
socket.end('ok');
});
});
const ipv6Server = net.createServer((socket) => {
hits.ipv6 += 1;
socket.end('ok');
});
const dnsServer = dgram.createSocket('udp4');
dnsServer.on('message', (message, remote) => {
hits.dns += 1;
const response = Buffer.from(message);
response[2] = 0x81;
response[3] = 0x83;
response.writeUInt16BE(0, 6);
response.writeUInt16BE(0, 8);
response.writeUInt16BE(0, 10);
dnsServer.send(response, remote.port, remote.address);
});
const servers = [httpServer, rawServer, ipv6Server, dnsServer];
try {
const httpAddress = await listen(httpServer, '127.0.0.1');
const rawAddress = await listen(rawServer, '127.0.0.1');
const ipv6Address = await listen(ipv6Server, '::1');
const dnsAddress = await listen(dnsServer, '127.0.0.1');
const directHttp = await new Promise((resolve, reject) => {
const request = http.get(`http://127.0.0.1:${httpAddress.port}/direct`, (response) => {
response.resume();
response.on('end', () => resolve(response.statusCode));
});
request.on('error', reject);
});
assert.equal(directHttp, 200);
assert.equal(await rawExchange('127.0.0.1', rawAddress.port, 'parent'), 'ok');
assert.equal(await rawExchange('::1', ipv6Address.port, 'ipv6'), 'ok');
const childResult = await runNode(`
const net = require('node:net');
const socket = net.createConnection({ host: '127.0.0.1', port: ${rawAddress.port} });
socket.on('connect', () => socket.end('child'));
socket.on('close', () => process.exit(0));
socket.on('error', (error) => { console.error(error); process.exit(1); });
`, {
HTTP_PROXY: 'http://127.0.0.1:9',
HTTPS_PROXY: 'http://127.0.0.1:9',
NO_PROXY: '',
});
assert.equal(childResult, '');
const resolver = new Resolver();
resolver.setServers([`127.0.0.1:${dnsAddress.port}`]);
await resolver.resolve4(`dns-${SECRET.toLowerCase()}.invalid`).catch(() => {});
assert.deepEqual(hits, { http: 1, raw: 1, childRaw: 1, ipv6: 1, dns: 1 });
return {
demonstrated: ['node:http', 'raw-tcp-ipv4', 'spawned-child-raw-tcp', 'raw-tcp-ipv6', 'direct-dns'],
meaning: 'these transports bypass HTTP_PROXY and must be stopped by the OS boundary',
};
} finally {
for (const server of servers) {
try { close(server); } catch {}
}
}
}
if (process.env.MAESTRO_EGRESS_PROXY_CHILD === '1') {
const port = Number(process.env.MAESTRO_EGRESS_PROXY_PORT);
assert.ok(Number.isInteger(port) && port > 0);
const childResult = await runActualEnvProxyChild(port);
const childOutput = JSON.stringify(childResult);
assert.ok(!childOutput.includes(SECRET));
console.log(childOutput);
process.exit(0);
}
const templates = await validateTemplates();
const envProxyRuntime = await runActualEnvProxyCheck();
const proxyPolicy = await runProxyChecks();
const proxyScope = await runDirectBypassChecks();
const result = {
ok: true,
externalNetworkUsed: false,
hostIsolationApplied: false,
templates,
envProxyRuntime,
proxyPolicy,
proxyScope,
};
const output = JSON.stringify(result, null, 2);
assert.ok(!output.includes(SECRET));
console.log(output);