santifer/career-ops · error
Access denied: Egress guard blocked private target IP
Error message
Access denied: Egress guard blocked private target IP ${ip} What it means
After resolving the hostname via DNS, validateUrlSecurity() checks every returned address against IPv4 private/reserved ranges (loopback 127/8, private 10/8 and 192.168/16, shared 172.16/12, link-local 169.254/16) and blocks the fetch. This prevents DNS-based SSRF where a public-looking hostname resolves to an internal IP. The message names the exact offending IP.
Solutions
- Use a URL that resolves to a genuinely public IP (host the JD on a publicly reachable server).
- If the content is internal, fetch it yourself and pass the saved file or pasted text instead of a URL.
- If the DNS record is wrong (public domain → private IP), fix the DNS record and retry.
Example fix
// before node upskill.mjs --url https://intranet.example.com/jd # resolves to 10.0.0.5 // after node upskill.mjs --file ./jd.txt
Defensive patterns
Strategy: validation
Validate before calling
const { lookup } = await import('dns/promises');
const addrs = await lookup(new URL(url).hostname, { all: true });
const priv = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.)/;
if (addrs.some(a => priv.test(a.address))) console.error('hostname resolves to a private IPv4; use a public host or a file'); Try / catch
try {
await upskillFromUrl(url);
} catch (e) {
if (e.message.includes('blocked private target IP')) {
console.error('host resolves to a private IPv4; fetch the JD yourself and pass the file');
} else throw e;
} Prevention
- Check DNS resolution (nslookup/dig) for the hostname before using it as a URL source.
- Prefer file-based input for intranet or tunnel-hosted content.
- Be alert to split-horizon DNS resolving public names to private IPs inside corporate networks.
When it happens
Trigger: A JD URL whose hostname's DNS A record resolves to any 127.x, 10.x, 192.168.x, 172.16–31.x, or 169.254.x address when upskill attempts the fetch.
Common situations: Corporate split-horizon DNS resolving an intranet host to a private IP; a DNS rebinding or misconfigured public DNS record pointing at a private address; testing with a tunnel bound to 127.0.0.1 behind a public hostname.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- plugin egress: resolves to a blocked address ( ) — possible…
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IPv6
- Blocked request to restricted destination (DNS)
- Blocked request to restricted destination (DNS)
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/5181f29b3e573a7b.
Report an issue: GitHub.
Appendix: source
Thrown at upskill.mjs:874
const hostname = url.hostname;
if (hostname === 'localhost' || hostname.endsWith('.local')) {
throw new Error('Access denied: Localhost or internal domain target detected.');
}
let addresses;
if (dnsCache.has(hostname)) {
addresses = dnsCache.get(hostname);
} else {
addresses = await dns.resolve(hostname).catch(() => []);
const lookupRes = await dns.lookup(hostname).catch(() => null);
if (lookupRes) addresses.push(lookupRes.address);
dnsCache.set(hostname, addresses);
}
for (const ip of addresses) {
if (/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|169\.254\.)/.test(ip)) {
throw new Error(`Access denied: Egress guard blocked private target IP ${ip}`);
}
if (ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc00:') || ip.startsWith('fd00:')) {
throw new Error(`Access denied: Egress guard blocked private target IPv6 ${ip}`);
}
}
return url.toString();
}
// --- CLI ---
// Everything below runs ONLY when upskill.mjs is the process entry point.
//
// Without this guard the module tail was unconditional, so `import
// { knownSkillsText } from './upskill.mjs'` re-parsed the IMPORTER's argv and ran
// one of these branches. That made the pure helpers above un-unit-testable despite
// their "exported for unit testing" docblocks — every assertion about them had to
// live inside --self-test.
//
// Under tests/ it also broke the harness, because test-all.mjs imports discoveredView on GitHub (pinned to aac998c7ed)