Lambda Outbound Proxies: Egress Allowlists That Block SSRF Paths

Lambda Outbound Proxies: Egress Allowlists That Block SSRF Paths

SSRF from a Lambda is boring until it hits 169.254.169.254, an internal Redis, or a cloud metadata flavor you forgot. The unfair advantage is forcing all egress through a centralized proxy with an explicit domain allowlist — application code cannot dial arbitrary IPs even if a prompt injection or open redirect tells it to.

⚡ TL;DR: VPC Lambdas with no NAT to the world; only routes to an egress proxy (or VPC endpoints + proxy). Proxy enforces HTTPS allowlist hosts; deny link-local, ULA, and RFC1918 by default. Inject HTTPS_PROXY at runtime; block raw sockets via seccomp where feasible. Pair with Agent Tool Allowlists, Secret-Aware Context Filters, and Secure AI Sandboxes.

Network topology that makes bypass hard

Lambda ENI -> private subnets
  NO default route to NAT for app subnets
  Routes: VPC endpoints (S3/STS/Secrets) + proxy TG
Proxy (egress VPCs) -> NAT -> Internet
  Allowlist: api.stripe.com, api.github.com, ...
  Deny: 169.254.0.0/16, 10.0.0.0/8, 127.0.0.0/8, metadata hostnames
// GOOD: force undici/node through proxy
process.env.HTTPS_PROXY = process.env.EGRESS_PROXY_URL;
process.env.NO_PROXY = "169.254.169.254,localhost,10.0.0.0/8"; // still deny at proxy

// BAD: app constructs http.request to user-supplied URL with no allowlist
import http from "node:http";
export function fetchUserUrl(url: string) {
  return http.get(url); // SSRF classic
}

✅ Central proxy allowlist is source of truth.
❌ Per-function ad-hoc if (!url.startsWith("https://api.")) only.

Proxy policy sketch

# illustrative Envoy/RBAC-ish policy
allow_hosts:
  - api.stripe.com
  - hooks.slack.com
  - bedrock-runtime.us-east-1.amazonaws.com
deny_cidrs:
  - 169.254.0.0/16
  - 10.0.0.0/8
  - 172.16.0.0/12
  - 192.168.0.0/16
  - 127.0.0.0/8
require:
  - tls: true
  - methods: [GET, POST, PUT]

Log every deny with source function name (from mTLS client cert or SigV4 to proxy). That telemetry feeds incident response when an agent tool starts probing.

Application hardening on top of network controls

import { Agent, fetch } from "undici";

const ALLOW = new Set(["api.stripe.com", "hooks.slack.com"]);

export async function safeFetch(input: string, init?: RequestInit) {
  const u = new URL(input);
  if (u.protocol !== "https:") throw new Error("https_only");
  if (!ALLOW.has(u.hostname)) throw new Error(`host_not_allowed:${u.hostname}`);
  if (u.hostname.endsWith(".internal")) throw new Error("internal_denied");
  return fetch(u, {
    ...init,
    dispatcher: new Agent({ connect: { timeout: 5_000 } }),
    redirect: "error", // open redirects are SSRF helpers
  } as any);
}

Defense in depth: network deny + app allowlist + no redirects. For coding agents, tool URLs must pass the same gate — see Agent Tool Allowlists.

Closing checklist

  • [ ] App subnets lack direct NAT; egress only via proxy or VPC endpoints
  • [ ] Proxy deny list covers link-local and RFC1918
  • [ ] Domain allowlist managed as code with review
  • [ ] HTTPS_PROXY injected; redirects disabled for untrusted URLs
  • [ ] Deny logs include function identity
  • [ ] Periodic probe tests expect deny on metadata IP

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a Reply