Choosing the Layer for Egress Filtering

March 12, 2029 · 12 min read

Advanced Networking · ANS-C01 · part of The Exam Room

The situation

The environment:

  • Roughly 200 EC2 instances and ECS tasks across 5 production VPCs, each needing outbound HTTPS to a curated list of destinations: an artifact registry, a cloud vendor’s APIs, the monitoring vendor, a handful of third-party API providers.
  • Each workload authenticates to specific endpoints; many use bearer tokens in Authorization headers.
  • Compliance wants: “prove you cannot exfiltrate data to a destination not on the allowlist,” with monthly evidence. They also want the ability to ban entire URL paths (not just hostnames) on certain destinations.
  • Operations wants: minimal latency impact, minimal new infrastructure to run, good audit logs.

Options:

  • AWS Network Firewall with domain-list rules (SNI matching), per-destination allowlist.
  • Squid forward proxy fleet. HTTPS CONNECT for opaque tunnelling, optional MITM for full inspection.
  • Third-party proxy appliance (ZScaler, Umbrella, McAfee, etc.) deployed as EC2 or SaaS.
  • Combined approach. Network Firewall for broad deny-by-default, Squid behind it for URL-path rules on specific destinations.

What actually matters

Before choosing, it’s worth understanding what each layer of inspection can actually see.

Layer 3-5 (5-tuple + TLS metadata) reads:

  • The 5-tuple: source IP, destination IP, protocol, source and destination ports.
  • TLS SNI from the ClientHello (layer-5 metadata in the first client-side TLS record).
  • HTTP headers including Host (for unencrypted HTTP).
  • TLS handshake fingerprints if enabled.

It does not read:

  • Encrypted TLS payload.
  • The URL path (in HTTPS; it sees SNI for the hostname, but everything after https://example.com/ is encrypted).
  • The HTTP method, headers, body of an HTTPS request.
  • The response body.

Layer 7 (terminating proxy) reads:

  • Everything the lower layers read, plus:
  • The full URL path on HTTP CONNECT, but only the host and port, the proxy-established tunnel is opaque payload after.
  • For TLS-terminating mode (proxy terminates, re-originates with its own cert chain that the client trusts): every header, URL, method, body, response, essentially everything in the HTTP exchange. Comes with a real trade-off: the proxy’s CA cert must be trusted by every client; certificate-pinning workloads break; privacy implications are real; decrypting TLS has performance cost.

Which layer matches the requirement?

If the allowlist is “these hostnames only,” SNI-level inspection is sufficient. SNI matching enforces that only TLS connections to allowed hostnames complete the handshake. Blocked connections are logged with source, destination, SNI, and rule.

If the allowlist is “these hostnames, and for some of them only these URL paths,” no SNI-level layer can enforce URL-path rules on HTTPS because the path is encrypted. A terminating proxy can; a hostname allowlist cannot.

If the workloads use certificate pinning (which security-conscious SDKs increasingly do), TLS termination breaks them. You’re limited to CONNECT-style proxying, which sees the hostname and port but nothing inside.

If the concern is “data exfiltration through legitimate destinations”, someone using an allowlisted service to upload stolen data, only full-payload inspection catches that, and only where pinning doesn’t intervene.

What we’ll filter on

  1. Hostname allowlisting, block by destination name, not IP?
  2. URL-path-level policy, deny POST /secret on an allowed host?
  3. Works with certificate-pinned clients, no MITM required?
  4. Operational burden, how much infrastructure to run?
  5. Audit trail depth, how rich is the log record?

The egress-filter landscape

1. AWS Network Firewall. Managed, sees SNI, blocks by hostname. Can’t see URL paths for HTTPS. Strong on low ops, partial on policy granularity.

2. Squid CONNECT proxy. Sees destination hostname:port from the CONNECT method, tunnels the rest opaquely. Logs destination and bytes. Clients must be configured with HTTPS_PROXY. Equivalent SNI-layer policy; more configurable; operational burden.

3. Squid MITM proxy (or any vendor proxy with TLS inspection). Terminates TLS, inspects everything, re-originates with a proxy CA. Full URL-path and header policy. Breaks certificate pinning. Privacy and trust implications. Higher throughput cost.

4. Third-party proxy (SaaS or vendor appliance). ZScaler, Umbrella, etc. Usually CONNECT-based for privacy reasons; some offer TLS inspection. SaaS reduces AWS-side operations but introduces a vendor dependency.

5. Combined Network Firewall + proxy. Network Firewall for broad deny-by-default; proxy fleet behind it for URL-path rules on selected destinations.

Side by side

Option Hostname allowlisting URL-path policy Works with cert pinning Ops burden Audit trail
Network Firewall ✓ (SNI) ✗ (HTTPS) Low Good (SNI, 5-tuple)
Squid CONNECT ✓ (Host:Port) ✗ (HTTPS) Medium Good (Host, bytes)
Squid MITM Medium-high Excellent (full HTTP)
Third-party SaaS Depends Depends Low (AWS-side) Vendor-specific
Combined NFW + proxy ✓ (via proxy) ✓ (if proxy is CONNECT) Medium Layered logs

Reading by requirement:

  • “Block by hostname, log the blocks, don’t break anything” → Network Firewall.
  • “Block by URL path too, and be willing to run a proxy fleet” → Squid MITM (if pinning isn’t an issue) or combined NFW+proxy.
  • “Don’t want to run infrastructure; pay vendor” → SaaS third-party.

Two layers, two views

Network Firewall view (layer 3-5) TCP handshake src IP:port → dst IP:port protocol: TCP destination :443 TLS ClientHello SNI: api.github.com (visible) JA3 fingerprint (visible) cipher list, supported versions Application bytes after handshake GET /repos/x (ENCRYPTED) Authorization: bearer xxx (ENCRYPTED) response body (ENCRYPTED) NFW decision rule: pass tls sni "api.github.com" PASS (SNI matches) no visibility into what's requested inside the TLS session cannot see URL path Forward proxy (MITM) view (layer 7) Client side of proxy client trusts proxy CA TLS to proxy cert proxy decrypts Decrypted request Host: api.github.com (visible) GET /repos/x/commits (visible!) Authorization: bearer ... (VISIBLE) Proxy-originated request new TLS session to github proxy presents real cert chain response decrypted, logged, re-encrypted Proxy decision rule: allow GET /repos/* deny POST /admin/* PASS (path matches allow) breaks certificate-pinned clients; privacy implications
Network Firewall sees the SNI and the 5-tuple; everything inside the TLS session is opaque. A MITM forward proxy decrypts and re-encrypts, seeing every byte. Each comes with different trade-offs.

The pick(s) in depth

Network Firewall first. Forward proxy only if URL-path policy is genuinely required. The shape that matches most compliance requirements without the MITM footprint.

Network Firewall setup was covered extensively in the stateful-rules post earlier in this series; the short version for egress filtering:

  • Deploy Network Firewall in the egress VPC; route workload VPCs through it.
  • Stateful rule group with a domain allowlist (pass tls rules for each allowed SNI, a default drop at the bottom).
  • Alert logs to CloudWatch; flow logs to S3.
  • Per-environment policies if prod and non-prod have different allowlists.

Forward proxy setup (Squid, in explicit mode):

  • Deploy a Squid fleet on EC2 (or containers) behind an NLB in the egress VPC.
  • Workload clients set HTTPS_PROXY=http://proxy.internal:3128 in their environment.
  • Squid config:
    acl allowlisted_hosts dstdomain api.github.com registry.npmjs.org ...
    http_access allow allowlisted_hosts
    http_access deny all
    access_log /var/log/squid/access.log squid
    
  • For CONNECT mode (TLS tunnelling with no decryption), the above is sufficient. Squid sees the Host and Port from the CONNECT request and either permits the tunnel or denies.
  • For MITM mode (to see URL paths), add ssl_bump peek/bump configuration plus a proxy CA that clients trust. Requires careful consideration of certificate pinning, privacy, and operational complexity.

Combined approach, the practical best-of-both:

  • Network Firewall as the default-deny egress at the VPC egress point. Allowlist all destinations that clients need.
  • A proxy fleet for the subset of destinations where URL-path policy is required. Clients that need path-level policy are configured with HTTPS_PROXY pointing at the proxy; others go direct through Network Firewall.
  • This layers the defences: a rogue process that doesn’t honour HTTPS_PROXY still hits Network Firewall’s SNI-based allowlist and gets blocked if its destination isn’t approved.

What about the auth headers in logs? A MITM proxy sees Authorization: bearer xxx headers in decrypted requests. Logging them verbatim is a problem. Squid with access_log by default does not log request bodies or headers beyond URL, but the request URL can contain secrets (query-string tokens). Audit logging policy for a MITM proxy needs explicit thought: what fields are captured, what’s redacted, who can read the logs.

A worked comparison

A workload in production tries to POST /api/exfiltrate to api.allowlisted-saas.com, an allowed destination:

Network Firewall path:

  1. TCP SYN to api.allowlisted-saas.com:443 hits Network Firewall.
  2. ClientHello arrives with SNI api.allowlisted-saas.com. Rule matches; pass.
  3. HTTPS request proceeds opaquely. Network Firewall sees bytes flowing; doesn’t see POST, path, or body.
  4. Logs show: source, destination, SNI, pass. Not blocked.

MITM Forward Proxy path (if the client uses it):

  1. Client sends HTTP CONNECT to proxy.
  2. Proxy sees CONNECT api.allowlisted-saas.com:443. For MITM mode, proxy intercepts; for CONNECT-only mode, proxy tunnels.
  3. In MITM mode, proxy terminates TLS, sees POST /api/exfiltrate. Rule matches “deny POST /api/* on this host.” Block.
  4. Logs show: user, method, path, action. Blocked.

For exfiltration detection, the proxy wins. For blocking based on allowlist only, Network Firewall is equivalent and simpler.

What’s worth remembering

  1. Network Firewall sees SNI; a forward proxy in MITM sees everything. If the policy is “allow these hostnames,” Network Firewall suffices. If the policy is “allow these URL paths,” only a MITM proxy (or unencrypted HTTP) can enforce.
  2. SNI inspection does not decrypt. The SNI is unencrypted metadata in the ClientHello. Network Firewall reads it and matches against the allowlist without any crypto intervention.
  3. MITM breaks certificate pinning. Clients that pin the real destination’s certificate cannot tolerate a proxy presenting a different one. Common in security-sensitive SDKs; increasing over time. Evaluate before committing to MITM.
  4. CONNECT-mode proxies are equivalent to Network Firewall’s SNI for visibility. Both see the hostname; neither sees inside the TLS session. Operational choice, not a visibility difference.
  5. Proxies require client configuration. Workloads need HTTPS_PROXY or equivalent; Network Firewall is transparent to the client. Transparency is an operational win; control is a trade.
  6. Layering them is the realistic answer in regulated environments. Network Firewall for the broad allowlist; proxy for path-level policy on specific destinations; logging from both feeding the SOC.
  7. Audit log content differs. Network Firewall logs 5-tuple + SNI + rule. Proxy logs 5-tuple + method + URL + sometimes headers. Both to CloudWatch / S3 / Firehose.
  8. Third-party SaaS shifts the operational burden to a vendor. ZScaler, Umbrella, etc. offload the proxy fleet; they add a dependency and an egress hop that may affect latency.

“Block outbound traffic we don’t want” is a simple sentence with two very different technical answers depending on the layer we inspect at. Network Firewall at SNI-layer is cheaper to run and never breaks a client. Forward proxy at L7 sees everything but demands client configuration and can break certificate-pinned clients if it MITMs. The work isn’t picking the fanciest option, it’s matching the inspection layer to the granularity of the policy, and layering when neither layer alone is enough.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.