← Back to blog
Cloud / AWSMay 7, 2026· 10 min

VPC Network Security Deep-Dive: Flow Logs, Security Groups vs NACLs, and Egress Filtering

A working engineer's guide to locking down VPC traffic: where stateful security groups beat stateless NACLs, why default-allow egress is a data-exfiltration liability, and how to hunt anomalous outbound flows in CloudWatch Logs Insights.

VPC Network Security Deep-Dive: Flow Logs, Security Groups vs NACLs, and Egress Filtering

Most VPC breakdowns happen on the way out, not the way in. Attackers who land on an instance, a compromised dependency in your build pipeline, or a leaky Lambda all need the same thing next: a path to the internet to stage tooling, beacon to command-and-control, and exfiltrate data. The default AWS VPC posture quietly gives them that path. This article walks through the three controls that actually constrain east-west and outbound traffic at the network layer: security groups, network ACLs, and VPC Flow Logs, and how to wire them into a default-deny egress design that you can verify rather than assume.

Stateful security groups vs stateless NACLs

Security groups are stateful and attach to elastic network interfaces (ENIs), not subnets. When an allowed connection is established outbound, the return traffic is permitted automatically regardless of inbound rules, because the connection is tracked. Security groups are allow-only: there is no deny rule, and rules are evaluated as a union across all groups attached to the ENI. This is why they are the right place to express application intent (this ENI may talk to that ENI on 5432) and the wrong place to express broad blocklists.

Network ACLs are stateless and attach to subnets. Statelessness is the detail that bites people: a NACL that allows inbound 443 will still drop the response to an outbound request unless you also open the ephemeral return range (typically 1024-65535) in the opposite direction. NACLs evaluate numbered rules in order, first match wins, and they support explicit DENY. That makes them useful as a coarse, subnet-wide guardrail, blocking a known-bad CIDR, or hard-stopping a port across an entire tier, that no single team can accidentally punch a hole through with a security group.

  • Use security groups for the 95 percent case: per-workload, intent-based allow rules referencing other security group IDs rather than CIDRs.
  • Use NACLs as a blast-radius backstop: explicit subnet-wide denies, RFC1918 boundary enforcement, and quarantine of a compromised subnet during incident response.
  • Remember the connection-tracking nuance: security groups exempt some flows (and very-high-rate flows) from tracking, which can surprise you when you expect automatic return traffic. NACLs always need both directions stated explicitly.

The open-egress problem

Every default security group ships with an outbound rule of 0.0.0.0/0 on all ports, and most teams never touch it. That single rule is the difference between a contained compromise and a full exfiltration event. With open egress, a compromised process can resolve an arbitrary domain, open 443 to an attacker-controlled host, and stream your database out over TLS that looks indistinguishable from normal API traffic. C2 frameworks lean on exactly this: long-lived outbound 443 or 53 sessions to hosts you never intended to reach. Ingress filtering does nothing to stop it, because the instance initiated the connection.

Default-deny egress is the single highest-leverage VPC control most teams skip. Replace the 0.0.0.0/0 outbound rule with an explicit allowlist of destinations your workload actually needs, prefix lists for AWS service ranges, peered VPC CIDRs, and a short set of known partner endpoints. Everything else should be denied by default and surfaced as a Flow Log REJECT you can alert on.

VPC endpoints: take the internet out of the path

You cannot allowlist what you do not have to reach. Gateway endpoints for S3 and DynamoDB, and interface endpoints (AWS PrivateLink) for services like Secrets Manager, KMS, ECR, STS, and CloudWatch Logs, keep that traffic on the AWS network and off the internet entirely. The security win is structural: with endpoints in place you can drop the NAT gateway for those flows, shrink your egress allowlist to almost nothing, and attach endpoint policies that restrict which principals and resources are reachable. An attacker on a private-subnet instance with no NAT route and only S3/KMS/ECR endpoints has no general internet path to beacon over. That is defense-in-depth the network enforces for you, not a control someone has to remember to configure per workload.

Flow Logs: fields, limits, and what to capture

VPC Flow Logs are your record of what actually traversed the network, independent of whether a rule allowed it. Publish them to CloudWatch Logs or S3 (and consider Parquet on S3 for cost-efficient querying at scale). Capture the ACCEPT and REJECT actions both, REJECT tells you what your rules stopped, ACCEPT tells you what they let through, which is where exfiltration hides. Use a custom format to add fields beyond the v2 default: pkt-srcaddr and pkt-dstaddr (the real endpoints behind NAT), flow-direction, tcp-flags, and the traffic-path field that distinguishes internet egress from PrivateLink and intra-VPC paths.

  • Flow Logs are aggregated over an interval (default 10 minutes, or 1 minute) and sampled at the metadata level, they are not a full packet capture and will not show payload.
  • Some traffic is never logged: link-local 169.254.x.x, Amazon DNS at the .2 resolver, DHCP, and Windows license activation. Do not build detections that assume their absence is suspicious.
  • srcaddr/dstaddr reflect the NAT or endpoint-facing address, use pkt-srcaddr/pkt-dstaddr to attribute a flow back to the originating instance behind a NAT gateway.
  • Bytes and packets are per-flow within the aggregation window, ideal for spotting a single talker moving gigabytes that a connection count alone would miss.

Hunting anomalous egress in CloudWatch Logs Insights

The query below surfaces accepted outbound flows that moved large volumes of data or used unusual destination ports, the two cheapest signals for exfiltration and C2. Sort by bytes, group by the talking pair, and the top of the result set is your candidate list. Run it over a private-subnet log group where outbound internet traffic should be rare to begin with.

fields srcAddr, dstAddr, dstPort, bytes
| filter action = "ACCEPT"
| filter dstAddr not like /^10\./ and dstAddr not like /^172\.(1[6-9]|2[0-9]|3[01])\./ and dstAddr not like /^192\.168\./
| filter bytes > 10000000
      or (dstPort != 443 and dstPort != 80 and dstPort != 22 and dstPort != 53)
| stats sum(bytes) as totalBytes, count(*) as flows by srcAddr, dstAddr, dstPort
| sort totalBytes desc
| limit 50

Read the output as a triage funnel. A private instance pushing tens of gigabytes to a single external IP on 443 is a classic staged exfiltration, the volume is the tell, not the port. A low-byte, high-flow-count pattern to one destination on an odd port (say repeated short sessions to 4444 or 8443) looks like beaconing. Cross-reference the top dstAddr values against your egress allowlist: anything not on it is, by definition, traffic your default-deny posture should have stopped. For continuous coverage, promote the same logic into a metric filter or a scheduled query and alert when totalBytes per pair crosses a threshold, then enrich with pkt-srcaddr to name the real source instance.

Putting it together

The controls reinforce each other. Default-deny egress on security groups shrinks the set of destinations an attacker can reach; VPC endpoints remove the internet from the common paths so the allowlist stays small; NACLs give you a subnet-wide kill switch for incident response; and Flow Logs make the whole thing observable so deny is a fact you verify, not a hope. Start by turning on Flow Logs with a custom format everywhere, then walk one workload from open egress to an explicit allowlist and watch the REJECT lines appear. Each one is a path you just closed.

Learn it by doing

Spin up a real AWS security lab, or explore our training tracks.

24 people viewing now