Exam Room · Advanced Security Specialist

Controlling What a VPC Is Allowed to Reach

October 13, 2027 · 24 min read

Cloud Security · part of The Exam Room

The situation

A health-tech company processes claims in a VPC that holds patient data. A control from the auditor is explicit: workloads in that VPC may reach three named SaaS vendors, an internal API in another account, and AWS service endpoints. Nothing else, and the company must be able to demonstrate the restriction rather than assert it.

The current state is a NAT gateway and a default route to the internet. Security groups allow all outbound, which is the default nobody changed. The three vendors publish API hostnames that resolve to CDN addresses which change without notice, so the network team’s first attempt, an allow-list of IP ranges in a NACL, broke twice in a fortnight and was reverted.

Two further facts shape the answer. About 40 EC2 instances and a dozen Lambda functions in private subnets make these calls. And one vendor requires mutual TLS, so whatever sits in the path cannot be a proxy that terminates the connection unless it can carry a client certificate.

What actually matters

The first thing that matters is that the requirement is expressed in names and the primitives available are mostly addresses. Security groups and NACLs filter on IP and port, and a CDN-hosted API behind a rotating address set defeats them. Any control that satisfies this requirement has to evaluate the hostname, which narrows the field immediately.

The second is the difference between permitting traffic and inspecting it. A control that allows connections to api.vendor.example and nothing else satisfies the letter of the requirement. A control that also looks inside the connection can detect data leaving that should not, which the auditor did not ask for and which is the actual risk the requirement exists to manage. The two have different costs and different failure modes.

The third is that mutual TLS constrains the design more than anything else on the list. A control that terminates TLS to inspect payloads breaks a connection that authenticates with a client certificate, unless it holds the certificate itself, which moves a credential into the network layer. Knowing which flows need inspection and which need only permission is what avoids designing around the strictest case everywhere.

The fourth is that AWS service traffic should leave this problem entirely. Calls to S3, DynamoDB, KMS and the rest can go through VPC endpoints and never touch the egress path, which removes them from the allow-list, removes them from the NAT gateway bill, and lets an endpoint policy restrict which resources they can reach. That is a stronger control than a hostname allow-list and it applies to the largest share of the traffic.

Underneath it, whatever is chosen has to produce evidence. “Demonstrate the restriction” means logs an auditor can read, which rules out anything enforcing silently.

What we’ll filter on

  1. Can it evaluate a hostname, or only an address?
  2. Does it permit, or does it inspect what passes?
  3. Does it break mutual TLS?
  4. Does it produce logs an auditor accepts?
  5. What happens to traffic when the control fails?
  6. Does it scale to every VPC, or need building per VPC?

The landscape

Security groups and NACLs. Stateful and stateless filtering on address, port and protocol. They are the right tool for “this tier may reach that tier” and the wrong tool here, because they cannot express a hostname. Worth naming so the reason for rejecting them is on the record: a CDN-backed vendor API has an address set that changes, and an allow-list of ranges is an outage waiting for a vendor’s next deployment.

AWS Network Firewall. A managed firewall deployed into dedicated subnets, with route tables directing traffic through its endpoints. It runs stateless rule groups and stateful ones, the latter accepting Suricata-compatible rules. It supports domain-name filtering directly, which is the capability the requirement needs, and it can be configured to allow a named list and deny everything else. It inspects all IP traffic rather than just HTTP, logs alerts and flows to CloudWatch Logs, S3 or Firehose, and can be centralised behind a Transit Gateway so one firewall serves many VPCs.

Route 53 Resolver DNS Firewall. Filters DNS queries against domain lists, allowing or blocking resolution. It is simple, cheap, and applies at the VPC level with no routing changes. Its limit is that it controls name resolution rather than connectivity: a workload that connects to a literal IP address never asks DNS and is unaffected. It suits defence in depth and does not satisfy a control on its own.

A forward proxy. Squid or a commercial equivalent on EC2, with clients configured to use it. It understands hostnames, can allow-list by domain, and produces detailed logs. It is also a fleet to run, patch and scale, every client needs proxy configuration, and it is the option most likely to break mutual TLS if configured to intercept.

Gateway Load Balancer. Inserts a third-party virtual appliance transparently into the traffic path, distributing flows across a scaled fleet of appliances while preserving the original packet. It is the answer when the required inspection is something AWS does not provide and a vendor appliance does, and it brings that vendor’s licensing and operational burden with it.

VPC endpoints. Interface and gateway endpoints keep AWS service traffic off the egress path entirely. Endpoint policies restrict what can be done through them, which is how a VPC is stopped from reaching an S3 bucket outside the organisation. This is a stronger control than hostname filtering for the traffic it covers, because it is an authorisation decision rather than a name match.

Egress-only internet gateway. The IPv6 equivalent of a NAT gateway: outbound connections permitted, inbound blocked. It is about direction, not destination, and does not filter by name.

VPC Flow Logs. Records accepted and rejected flows with addresses, ports and bytes. It is the evidence layer for whatever enforces, and it records addresses rather than names, so on its own it will not show an auditor that only three vendors were reachable.

Evaluation

Side by side

Control Filters on Inspects payload Breaks mTLS Evidence Scope
Security group / NACL Address, port Flow logs Per ENI / subnet
Network Firewall Domain, address, protocol ✓ optional ✗ unless configured to Alert and flow logs VPC or centralised
DNS Firewall Query name Query logs VPC
Forward proxy Domain, URL ✓ if intercepting ✓ if intercepting Proxy logs Per client config
Gateway Load Balancer + appliance Whatever the appliance does Depends Appliance logs Transparent
VPC endpoints + policy Service and resource n/a CloudTrail Per service

Two rows do most of the work. VPC endpoints take AWS service traffic out of the problem with a stronger control than any of the others offer, and Network Firewall covers the vendor traffic with domain filtering, central deployment and logs. The proxy does the same job with a fleet to run, and Gateway Load Balancer is the escalation for inspection AWS does not provide.

The solution

Endpoints for AWS traffic, Network Firewall for everything else, DNS Firewall underneath as a second layer, and default-deny at the end rather than the beginning.

Start with the endpoints, because they remove the largest share of the traffic from the question. Gateway endpoints for S3 and DynamoDB cost nothing; interface endpoints for KMS, Secrets Manager, Systems Manager and the others the workload uses cost per hour and per GB. Attach endpoint policies restricting each to the resources the workload legitimately touches, which turns “this VPC may reach S3” into “this VPC may reach these buckets”. That is a better control than the auditor asked for and it is the one worth having.

Then deploy Network Firewall with dedicated firewall subnets in each Availability Zone, and repoint the private subnets’ default route through the firewall endpoints. Configure a stateful rule group with domain-name filtering: an allow list containing the three vendor hostnames and the internal API, and a deny-all beneath it. Because the vendors are reached by name and the rule evaluates the name, a vendor rotating CDN addresses changes nothing.

Add the DNS Firewall as a second layer rather than as the control. It blocks resolution of anything outside the allow list, which catches a class of exfiltration attempt earlier and cheaply, and it does nothing about a connection to a literal address. Two layers where one enforces connectivity and the other enforces resolution is defence in depth; using the DNS layer alone would be a control an attacker sidesteps by not asking.

The mutual TLS vendor decides how far inspection goes. Configure the firewall to permit that flow by domain without TLS inspection, so the client certificate handshake completes end to end, and accept that this flow is permitted rather than examined. The other two vendors can have deeper inspection if the risk assessment wants it. Making that distinction explicitly, and writing down why, is better than either inspecting nothing or breaking the vendor.

Turn on the logs before the enforcement. Send firewall alert and flow logs to CloudWatch Logs, and run the rule group in an alert-only posture for a fortnight first. This is what turns the deny-all from a change into a measurement: the log shows every destination the workload actually reaches, which will include several nobody remembered. Move to enforcement once that list is understood and the allow list has been corrected to match reality.

Finally, make it repeatable. Deploy the firewall centrally behind the Transit Gateway rather than per VPC, so the next VPC inherits the control instead of needing its own, and put the configuration in the pipeline so a rule change is reviewed like code.

Why not the forward proxy. It meets the requirement, and it means a fleet to patch and scale, client configuration everywhere including the Lambda functions, and the highest chance of breaking the mutual TLS vendor. Network Firewall is the managed version of the same idea.

Why not IP allow-lists after all. The team already tried, and CDN-hosted vendor APIs are the case the approach cannot survive. It is worth recording the attempt in the design document so the next person does not repeat it.

Worked example

The endpoint work removes 68% of egress bytes from the NAT gateway in the first week, which was not the goal and pays for a chunk of the firewall.

The fortnight of alert-only logging finds eleven destinations beyond the five expected. Three are package repositories used at instance boot. Two are the vendors’ status pages, called by a health check nobody documented. One is an analytics beacon in a JavaScript dependency, which becomes a separate conversation. The remaining five are AWS endpoints that were being reached over the internet path because no interface endpoint existed for them, which is a second round of endpoint work.

The allow list ends up with fourteen entries rather than five, each with a comment saying who asked for it. That list is the artefact the auditor actually wants, and it did not exist in any form before the logging.

Enforcement goes live on a Thursday. One thing breaks: an instance bootstrap script pulling from a repository mirror on an address rather than a name, which the domain rule cannot match and which had been invisible because it only runs on launch. It is fixed by pointing the script at the name.

The mutual TLS vendor works throughout, because that flow was permitted rather than inspected, and the decision is recorded in the risk register with the reason.

Six weeks later a developer adds a dependency that calls an unapproved endpoint. The firewall denies it, the log shows it, and the developer asks for the addition through the pipeline. That is the control functioning, and it is visible rather than silent.

What’s worth remembering

  1. Egress requirements are usually expressed in hostnames and security groups only understand addresses, so a CDN-hosted vendor API defeats an IP allow-list on the vendor’s next deployment.
  2. Take AWS service traffic out of the problem with VPC endpoints and endpoint policies; that is an authorisation control on which resources can be reached, which is stronger than matching a name.
  3. Network Firewall does domain-name filtering, inspects all IP traffic rather than just HTTP, and can be centralised behind a Transit Gateway so one deployment covers many VPCs.
  4. DNS Firewall controls resolution, not connectivity, so a workload connecting to a literal address is unaffected; it is a second layer rather than the control.
  5. Mutual TLS and payload inspection are in tension, because a control that terminates TLS breaks a client-certificate handshake unless it holds the certificate; decide per flow and record the decision.
  6. Run in alert-only mode before enforcing. The log of what the workload actually reaches is both the corrected allow list and the evidence an auditor is asking for.

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