diff --git a/plugins/voyage/lib/exporters/endpoint-validator.mjs b/plugins/voyage/lib/exporters/endpoint-validator.mjs index da1a767..7603918 100644 --- a/plugins/voyage/lib/exporters/endpoint-validator.mjs +++ b/plugins/voyage/lib/exporters/endpoint-validator.mjs @@ -11,6 +11,19 @@ import { ok, fail, issue } from '../util/result.mjs'; const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '0.0.0.0']); const LINK_LOCAL_PREFIXES = ['169.254.', 'fe80:']; +// Cloud metadata service endpoints — PERMANENTLY blocked even with +// VOYAGE_OTEL_ALLOW_PRIVATE=1. These addresses expose IAM credentials, +// instance secrets, and user-data on AWS/GCP/Azure/AliCloud workloads. +// Operator-trust is NOT extended to these specific IPs because the +// blast-radius (cloud-account compromise) is qualitatively different +// from home-lab RFC-1918 access. +const HARD_BLOCKED_HOSTS = new Set([ + '169.254.169.254', // AWS / GCP / Azure metadata service + '100.100.100.200', // AliCloud metadata service + 'metadata.google.internal', + 'metadata.azure.com', +]); + function isRfc1918(host) { // 10.0.0.0/8 if (/^10\./.test(host)) return true; @@ -58,7 +71,15 @@ export function validateOtlpEndpoint(url, opts = {}) { // Strip brackets from IPv6 const host = parsed.hostname.replace(/^\[|\]$/g, ''); - // Cloud metadata IP (link-local) — always rejected unless explicit opt-in + // Cloud metadata services — PERMANENTLY blocked. VOYAGE_OTEL_ALLOW_PRIVATE + // does NOT override this; metadata endpoints expose IAM credentials. + if (HARD_BLOCKED_HOSTS.has(host)) { + return fail(issue('ENDPOINT_HARD_BLOCKED', + `Endpoint ${host} is permanently blocked (cloud metadata service). ` + + `VOYAGE_OTEL_ALLOW_PRIVATE does not override this restriction.`)); + } + + // Other link-local addresses — rejected unless explicit opt-in if (isLinkLocal(host) && !allowPrivate) { return fail(issue('ENDPOINT_LINK_LOCAL_REJECTED', `Link-local endpoint ${host} rejected (potential cloud-metadata access). ` + diff --git a/plugins/voyage/tests/hooks/otel-export-validators.test.mjs b/plugins/voyage/tests/hooks/otel-export-validators.test.mjs index 43e4974..c28ca07 100644 --- a/plugins/voyage/tests/hooks/otel-export-validators.test.mjs +++ b/plugins/voyage/tests/hooks/otel-export-validators.test.mjs @@ -74,10 +74,34 @@ test('path-validator: FORBIDDEN_PREFIXES exports drift-pin', () => { // ---- endpoint-validator: CWE-918 mitigation ------------------------------- -test('endpoint-validator: rejects http://169.254.169.254/ link-local (cloud metadata, CWE-918)', () => { +test('endpoint-validator: rejects http://169.254.169.254/ — PERMANENTLY blocked (CWE-918 cloud metadata)', () => { const r = validateOtlpEndpoint('http://169.254.169.254/v1/metrics', { env: {} }); assert.equal(r.valid, false); - assert.ok(r.errors.find(e => e.code === 'ENDPOINT_LINK_LOCAL_REJECTED')); + assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED')); +}); + +test('endpoint-validator: 169.254.169.254 stays blocked EVEN WITH VOYAGE_OTEL_ALLOW_PRIVATE=1', () => { + // Cloud metadata service is qualitatively different from RFC-1918 home-lab + // access — operator-trust is NOT extended here. AWS/GCP/Azure metadata + // exposes IAM credentials and can compromise the entire cloud account. + const r = validateOtlpEndpoint('http://169.254.169.254/v1/metrics', + { env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } }); + assert.equal(r.valid, false, 'cloud metadata MUST stay blocked even with opt-in'); + assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED')); +}); + +test('endpoint-validator: AliCloud metadata 100.100.100.200 PERMANENTLY blocked', () => { + const r = validateOtlpEndpoint('http://100.100.100.200/latest/meta-data', + { env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } }); + assert.equal(r.valid, false); + assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED')); +}); + +test('endpoint-validator: metadata.google.internal hostname PERMANENTLY blocked', () => { + const r = validateOtlpEndpoint('http://metadata.google.internal/computeMetadata/v1', + { env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } }); + assert.equal(r.valid, false); + assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED')); }); test('endpoint-validator: rejects http://example.com/ (requires https)', () => {