The pattern I keep seeing in internal audits and post-incident reviews is simple: outbound traffic is left “open by default” and then people try to compensate with detection. In cloud, that ordering is usually expensive. If a VM/EC2 is compromised, unrestricted egress (for example, a Security Group with outbound to 0.0.0.0/0) becomes the fast path for Command & Control and exfiltration: HTTP/HTTPS to any destination, DNS to external resolvers, and tunnels that masquerade as legitimate traffic.
This article focuses on how to implement real Network Egress Control for VPCs: not as a “checkbox”, but as a set of operable guardrails that withstand incidents, changes, and pressure to “not break” dependencies.
What went wrong when egress is “allow everything”
The problem is not that internet egress exists; the problem is that egress is not governed. In many VPCs, private instances go out through a NAT Gateway without restrictions, and Security Groups allow egress to any destination. Day to day it seems practical: updates, API calls, telemetry… everything “works”. But that same convenience removes friction for the attacker.
In an enterprise, the real impact shows up in two moments: when there is a compromise and when you have to respond quickly. With open egress, malware can rotate C2 domains/IPs unimpeded and exfiltrate over HTTPS to an external bucket, a pastebin, a server in a cheap ASN, or even to a CDN. The worst part is that, from the network perspective, the traffic “looks normal”: TLS on port 443.
A common collateral effect is operational: when the decision is finally made to close it down, no one has an inventory of outbound dependencies. The product team experiences the change as “security breaking things”, and the result is usually reopening 0.0.0.0/0 “temporarily”. That temporariness tends to last months.
Architecture decisions: NAT with restrictions vs. private endpoints
The first real trade-off is separating traffic “necessary to operate” from “wildcard” traffic. For managed services of the cloud provider itself, the best lever is to avoid the internet directly. In AWS, this is implemented with VPC Endpoints (Gateway Endpoints for S3/DynamoDB and Interface Endpoints/PrivateLink for services such as KMS, Secrets Manager, STS, ECR, etc.). When you do it well, you reduce surface area: that traffic no longer depends on the NAT or on routes to an Internet Gateway.
In corporate scenarios, the change shows up in incident response: if a compromised instance tries to exfiltrate to S3, but your allowed access to S3 is constrained to private endpoints and specific buckets, it is no longer “S3 to anywhere”. Also, by moving internal dependencies to PrivateLink, you eliminate the need to open egress to public ranges that change.
- Gateway Endpoints (S3/DynamoDB): reduce cost and simplify routing, but require discipline with endpoint policies and bucket policies. Without that discipline, you are still allowing broad access, just “internally”.
In practice, this implies verifying that access to S3/DynamoDB from private subnets cannot go “over the internet” as a bypass. If you have a route to the NAT and also an endpoint, a compromised app can still go out through the NAT to external destinations; the endpoint only covers the associated services.
- NAT Gateways with restrictions: useful for unavoidable external dependencies (third-party APIs, repositories, license validators), but require an additional control point (firewall/proxy) because NAT alone does not filter by FQDN or perform inspection.
The typical mistake is assuming that “having NAT” is equivalent to “having control”. If there is no inspection or allowlist, NAT is just a highly available outbound door.
How to do it in practice: cutting egress in Security Groups without paralyzing teams
The quick win with the best impact/effort ratio is to stop treating egress as a “permit all” and move to an allowlist model based on known dependencies. In AWS, this starts by hardening workload Security Groups: allow outbound only to the necessary ports and destinations (for example, 443 to an internal proxy, 5432 to a database, or 443 to a private endpoint).
In environments with multiple squads, it works better if you turn it into a platform standard: SG templates by workload type, and a controlled path to request exceptions. If you do it “by hand” on each instance, it breaks with the first autoscaling event or the first redeploy.
- Operational pattern: force the only allowed egress from private subnets to be to a proxy/firewall (port 3128/443 depending on design) and to necessary internal services (DBs, caches). This drastically reduces direct C2 from the instance.
This pattern creates one good consequence and one inconvenient one. The good one: the attacker can no longer “talk to the world” from the machine. The inconvenient one: if an app needs to talk to a new third party, it will be noticeable. That is why the key is the dependency onboarding process and observability (seeing what gets blocked and why).
- Quick validation in AWS: check that SGs do not have broad egress to
0.0.0.0/0on 443 “just in case”. In VPC Flow Logs, validate that outbound flows to the internet from private subnets shrink to “proxy/firewall only” and that no alternative routes exist (for example, instances with a public IP by mistake).
If “intermittent” production errors appear after the change, it is usually because there were undocumented dependencies (telemetry, OCSP/CRL, authentication endpoints, repositories). That is not an argument to open everything: it is a sign that egress control is forcing you to uncover real technical debt.
Inspection and blocking: outbound proxy and firewalls with maintainable policies
When you need fine-grained control (domains, categories, TLS inspection in justified cases), egress must go through an inspectable point. In AWS, a common pattern is to deploy an outbound proxy (for example, Squid) in a management or egress subnet, and force routes from workloads toward that proxy. In organizations with stricter requirements, centralized control with AWS Network Firewall is preferred; in Azure, the typical counterpart is Azure Firewall.
The practical difference between “blocking by IP/port” and “blocking by domain” shows up the day a third party migrates IPs or uses a CDN. If your allowlist is only IP-based, you will break integrations frequently. If you rely only on FQDN without good governance, you will open too much (“*.amazonaws.com” and similar). A maintainable policy is usually: private endpoints for your own cloud services; and for third parties, FQDN/category rules with an onboarding and expiration process.
- What to block first: direct external DNS resolvers (to avoid bypass), direct egress to 443 from workloads (to force proxy/firewall), and “commodity” exfil destinations (generic storage, paste sites) according to corporate policy.
This is not theory: in real incidents, if you allow direct DNS to the internet, an attacker can use DoH/DoT or tunnel in subdomains to evade IP-based controls. Forcing controlled DNS and controlled egress reduces the room to maneuver.
- Operational signals that the control works: connection attempts to unknown IPs from workloads drop; well-classified (and actionable) deny events rise in the firewall/proxy; and exceptions are managed as traceable changes, not as “open it and done”.
A frequent anti-pattern is deploying a firewall but leaving alternative routes: a subnet with a route to the NAT “for emergencies”, an instance with a public IP “temporarily”, or SGs with open egress “just for debugging”. In networking, one such exception invalidates the entire investment.
Recommendations for corporate environments
If you want real Network Egress Control for VPCs, the focus must be on closing the easy door (broad egress from the workload) and creating a governed outbound path (private endpoints + an inspectable point). The goal is not to eliminate the internet, but to prevent a compromised machine from improvising C2/exfil without friction.
In practice, the most effective approach is usually to combine: VPC Endpoints/PrivateLink for managed services, Security Groups with minimal allowlisting, and a proxy/firewall as the only outbound path for external dependencies. What makes this work in an enterprise is the operations: templates, an exception process, and continuous validation with Flow Logs and SG/route reviews to avoid bypass.
If today there is egress to 0.0.0.0/0 “by default”, the first step is not to argue about tools, but to decide where the control will live (proxy/firewall/endpoints) and move traffic there, in a visible and reversible way. From that point on, egress stops being a backdoor and becomes a managed component.
Interested in Cloud Security?
Technical analysis, hands-on labs and real-world cloud security insights.