The rule that was never consulted
Picture the artifact: a tightly written egress rule, scoped to a single destination CIDR, reviewed in a change window, attached to an instance running production workloads. Someone on the team points to it as proof the workload is locked down. Two weeks later, credential material surfaces in an attacker's hands — pulled from the instance metadata endpoint. The egress rule was never the problem. The problem is that no security group was ever consulted for that traffic.
Cloud network primitives — security groups, network ACLs, and higher-order policy constructs — each carry a specific promise about what traffic they can shape. When a workload reaches something outside the expressed scope of those primitives, the promise holds: the rules worked exactly as designed. The gap is in the design, not the enforcement.
This is not a misconfiguration story. It is a semantic story. Each primitive can express a specific vocabulary of requirements; requirements outside that vocabulary are simply not covered. Knowing what each one can and cannot say is the operational prerequisite for designing controls that do not leave systematic gaps.
Stateful and stateless — and why it matters
AWS documents the core distinction plainly: security groups are stateful, meaning response traffic to an allowed request is automatically permitted regardless of inbound rules (Source: docs.aws.amazon.com, https://docs.aws.amazon.com/vpc/latest/userguide/vpc-security-groups.html).
That single fact reshapes how you reason about stateless list evaluation. A stateless primitive — a network ACL, or a firewall policy evaluated per-packet — sees every packet independently. It has no memory of the session that produced it. The consequence is that a stateless list needs explicit rules for both directions of any session: an inbound allow for the request and an outbound allow for the response, or vice versa. Omit the return-path rule and the session fails, even though the initiating traffic was permitted.
The security group's statefulness means you can write rules that express intent — "this instance may initiate connections to this destination" — without reasoning about the TCP handshake's return leg. For stateless constructs, intent and packet-level mechanics are the same problem, and the rule set has to express both.
Connection tracking, the mechanism behind that statefulness, carries a second operational benefit: during incident response, the tracked session table tells you which flows were active at a point in time, which instance initiated them, and how long they persisted. That record is often more immediately actionable than raw packet logs, because it reflects established sessions rather than individual packets. Containment also benefits: terminating a tracked session drops both directions without requiring a matching rule change on the return path.
The decision question here is not which model is better. It is which requirement you are expressing. If the requirement is "permit a service to respond to its callers," a stateful primitive carries that correctly and compactly. If the requirement is "block a specific return path regardless of session origin," only a stateless primitive can say that.
What each primitive can express
Security groups express membership. A rule can reference another security group as its source or destination, which lets you write requirements like "this database tier may receive traffic from this application tier" without encoding IP addresses that change as instances scale. Security groups are allow-only constructs: there is no mechanism for writing an explicit deny. If no rule permits a flow, the flow is dropped. Permitted flows are the only expressible vocabulary.
Network ACLs are ordered lists. They can express both allow and deny, and they evaluate in rule-number order, stopping at the first match. That ordering is both the power and the failure mode: a deny rule only works if it precedes any allow rule that would otherwise match the same traffic. Swapping rule numbers during a maintenance window can silently invert the policy.
Higher-order constructs divide into two distinct categories that are frequently conflated. Data-path controls — AWS Network Firewall, Gateway Load Balancer appliances — sit inline in the routing path and evaluate traffic as it flows. They can enforce protocol, identity, and domain-based policy, and their decisions are binding because traffic physically traverses them. Service control policies (SCPs), by contrast, are IAM authorization guardrails operating at the AWS Organizations level.
They constrain which API operations an identity can call, not which packets can flow. A SCP that denies ec2:DescribeInstances prevents that API call; it says nothing about TCP flows between subnets. Treating SCPs as data-path controls produces a false sense of network coverage. Neither category substitutes for the lower primitives when those primitives are the enforcement point.
The practical gap between these models: if your requirement is "deny this specific source CIDR regardless of any other rule," a security group cannot say it. If your requirement is "permit traffic from any instance that is a member of this logical group," a network ACL cannot say it without being rebuilt every time group membership changes.
The traffic no security group filters
AWS documents a specific set of destinations that security groups do not filter, regardless of what the rules say: Amazon DNS, DHCP, EC2 instance metadata, ECS task metadata endpoints, Windows license activation, the Amazon Time Sync Service, and reserved IP addresses used by the default VPC router (Source: docs.aws.amazon.com, https://docs.aws.amazon.com/vpc/latest/userguide/vpc-security-groups.html).
That list is not a bug. It is load-bearing infrastructure. Instances need DNS to resolve endpoints, DHCP to acquire addresses, and the time service to maintain certificate validity. Excluding this traffic from security group evaluation keeps the control plane functional.
Metadata endpoint exposure is the gap that matters most operationally. The specific attack vector is server-side request forgery (SSRF): an application that can be induced to make outbound HTTP requests on an attacker's behalf can be directed to 169.254.169.254, the EC2 instance metadata endpoint, where it will retrieve IAM credentials associated with the instance profile. A workload can reach that address regardless of the egress rules attached to it. No security group rule blocks that path. The egress rule that looked airtight simply does not apply.
IMDSv2 reduces but does not eliminate this exposure. It requires the caller to first obtain a session token via a PUT request, which a naive SSRF implementation cannot satisfy because most SSRF vectors can only issue GET requests. However, IMDSv2 does not protect against a sophisticated attacker who controls code executing on the instance, or against SSRF vectors capable of issuing PUT requests. IMDSv2 enforcement is a necessary control but not a sufficient one in isolation.
The full applicable control set for this requirement:
- IMDSv2 enforcement — require session-oriented requests at the instance or account level; blocks the majority of SSRF-based credential harvesting
- Instance profile scoping — limit the permissions attached to the instance role to the minimum required; reduces the blast radius when credentials are obtained
- Metadata endpoint hop-limit — setting the hop limit to 1 prevents metadata requests from being forwarded by containerized workloads or proxies
- Behavioral detection — monitor for unexpected credential use outside the instance's normal operating region or pattern
Network rules cannot solve this problem. If you need visibility into what is actually reaching the instance metadata endpoint, rely on dedicated host-level logging, proxy monitoring, or flow log analysis rather than perimeter controls.
DNS exemption carries a narrower but real operational consequence: a workload can issue DNS queries regardless of your egress rules. DNS-based data exfiltration — encoding data in query hostnames, then reading it at an attacker-controlled authoritative nameserver — does not require an open egress port to a remote IP address. Filtering that path requires a resolver-level control, such as Route 53 Resolver DNS Firewall, not a security group modification. The exemption covers DNS traffic to the VPC resolver; traffic directed at external resolvers on UDP/TCP 53 is subject to security group evaluation and can be blocked.
Choosing the primitive
The decision is not preference — it is the shape of the requirement.
| Requirement | Primitive that expresses it | Why the others cannot | What still is not covered |
|---|---|---|---|
| Permit traffic between logical tiers without encoding IP addresses | Security group (source group reference) | Network ACLs operate on CIDRs; they have no concept of group membership | The default security group ships with a rule permitting same-group traffic; custom security groups do not include this rule by default, and same-group traffic is blocked unless explicitly permitted |
| Explicitly deny a known-malicious source CIDR | Network ACL (ordered deny rule) | Security groups are allow-only; no explicit deny is expressible | The deny applies at the subnet boundary; traffic within the subnet is not subject to the ACL |
| Allow stateful service responses without writing return-path rules | Security group (stateful evaluation) | Network ACLs evaluate each packet independently; return traffic requires an explicit rule in the opposite direction | Session state is tracked at the security group level; the ACL still sees each packet separately |
| Enforce protocol-level deny for a specific port across a subnet, irrespective of instance-level rules | Network ACL (subnet-level ordered deny, numbered ahead of any allow for the same traffic) | Security groups are allow-only and attach per ENI; a subnet-wide posture would depend on every group on every ENI omitting the port, and attaching any one permissive group reopens it. A NACL deny applies to every ENI in the subnet, and no security group rule can loosen it | Rules must be ordered correctly; a misplaced allow rule above a deny is a silent policy inversion |
| Prevent workload access to the instance metadata endpoint via network rules | No primitive covers this requirement | Security groups do not filter metadata endpoint traffic by design; network ACLs operate at the subnet boundary, not the hypervisor link | IMDSv2 enforcement, hop-limit configuration, instance profile scoping, and behavioral detection are the applicable controls |
| Restrict egress to approved service endpoints by logical service identity rather than IP | Data-path controls (Network Firewall, Gateway Load Balancer appliances) with managed prefix lists | Neither security groups nor network ACLs have native awareness of service identity outside of AWS-managed prefix lists; raw CIDRs change. SCPs constrain API authorization, not packet flows | Managed prefix lists must be maintained; stale entries create gaps without alerting |
The primitive selection decision belongs at the requirement definition stage, not the implementation stage. By the time you are writing rules, the structural choice is already constraining what you can express.
A useful test before finalizing a control design: write the requirement as a sentence without using the word "rule." If the sentence contains the word "any instance in this group," a security group can carry it. If it contains "regardless of the requesting instance," you need a stateless primitive. If it contains "regardless of any other rule," you need a deny-capable construct — which means a network ACL with the deny numbered ahead of any NACL rule that would allow the same traffic, or an inline firewall in the routing path.
The harder test is the one in the final table row: write the requirement, then ask whether any available primitive is even in the traffic path. For metadata endpoint traffic, DNS queries to the VPC resolver, and DHCP negotiation, the answer is no — and the design has to account for that before the controls are considered complete.
What no combination of these primitives covers: application-layer intent. A security group that permits TCP 443 to a destination CIDR permits all HTTP/S semantics over that connection. What data moves, what credentials are presented, and what API operations execute are invisible to the network layer. That is not a gap in any one primitive — it is the boundary of what the network layer can express. Controls above that boundary belong to a different enforcement tier.
Sources
- docs.aws.amazon.com: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-security-groups.html