VPC Security Groups (SGs) and Network ACLs (NACLs) are the two packet-filtering primitives that AWS provides inside a VPC. They sit at different boundaries (ENI vs subnet), behave differently (stateful vs stateless), and are used together to form a layered network perimeter. This page covers the differences, common patterns (3-tier app, bastion, zero-trust egress), and the related primitives — VPC endpoints, PrivateLink, and SG references — that complete the picture.
1. Layered Enforcement
A packet from the internet to an EC2 instance traverses (in order) the route table, the subnet's NACL, the ENI's Security Group, and finally the instance's OS firewall. NACL and SG are AWS-managed; both must allow the traffic for it to reach the workload.
2. Security Groups (Stateful)
Stateful: if an inbound rule allows a connection, the return traffic is automatically allowed regardless of outbound rules. You don't need to model TCP / UDP reply ports.
Allow-only: there's no Deny — the absence of an Allow rule is the deny. To deny a specific IP within an otherwise-allowed CIDR, use a NACL or move to a different subnet.
Per-ENI attachment: SGs apply at the elastic network interface, not the instance. Up to 5 SGs per ENI (raise to 16 via quota request); rules are unioned.
Reference other SGs: the source / destination of a rule can be another SG ID — "allow from anything tagged app-sg." This is what makes SGs scale beyond CIDR-based ACLs.
Reference prefix lists: AWS-managed prefix lists (e.g., com.amazonaws.us-east-1.s3) and customer-managed prefix lists are referenced by ID, so the rule auto-updates when AWS changes the underlying CIDRs.
Default SG: every VPC has a default SG that allows all traffic between members of the same SG and all egress — do not use it for production workloads.
3. Network ACLs (Stateless)
Stateless: inbound and outbound are evaluated independently. To allow an inbound TCP/443 connection, you must also allow the outbound ephemeral-port reply (1024-65535).
Allow + Deny: rules are numbered (typically 100, 200, 300...) and the lowest number that matches wins. There's an implicit final * DENY ALL.
Subnet attachment: exactly one NACL per subnet; one NACL can serve many subnets. The default NACL allows all traffic in/out so newcomers don't break things accidentally.
CIDR-only: NACL rules can't reference SGs or prefix lists — you specify CIDRs directly. This is one reason most enforcement happens in SGs.
Use cases: emergency block of a single attacker IP across an entire subnet, hard separation of regulated subnets, defence-in-depth so that an over-permissive SG can't expose a subnet alone.
4. Comparison Table
┌────────────────┬────────────────────────────┬────────────────────────────┐
│ Aspect │ Security Group │ Network ACL (NACL) │
├────────────────┼────────────────────────────┼────────────────────────────┤
│ Scope │ Per ENI │ Per Subnet │
│ State │ Stateful │ Stateless │
│ Rules │ Allow only │ Allow + Deny │
│ Order │ All rules eval. │ Numbered, lowest first │
│ Default │ Deny all in/out │ Default NACL allows all │
│ Refs │ SG / Prefix Lists │ CIDR only │
│ Limit │ 60 rules each way │ 20 rules each way │
│ Use Case │ Per-workload allowlist │ Subnet block / quarantine │
└────────────────┴────────────────────────────┴────────────────────────────┘
5. VPC Endpoints & PrivateLink
VPC endpoints let workloads reach AWS services and SaaS providers without traversing the public internet. They eliminate the need for NAT gateways for AWS-service traffic and tighten the network blast radius.
Gateway endpoints: S3 and DynamoDB only. Free. Configured as a route in the route table; traffic to these services flows over the AWS backbone.
Interface endpoints (PrivateLink): ENIs in your subnets that surface a private IP for an AWS service or third-party SaaS (Snowflake, Datadog, Datadog APM, etc.). Charged per-AZ per-hour plus per-GB.
Endpoint policies: resource policies on the endpoint that restrict which principals / actions / resources can use it — e.g., "this S3 endpoint can only access buckets in our account."
VPC Endpoint Services (PrivateLink producer): expose your own NLB-fronted service to other VPCs / accounts privately. The consumer creates an interface endpoint pointing at your service.
Common pattern: use S3 Gateway endpoint plus interface endpoints for STS, SSM, KMS, and ECR so private subnets can run without a NAT gateway at all.
6. SG References & Chaining
The most powerful SG pattern is referencing one SG from another. Instead of pinning rules to CIDRs, you describe relationships between roles:
WebSG:
Ingress:
- { Protocol: tcp, Port: 443, Source: 0.0.0.0/0 }
Egress:
- { Protocol: tcp, Port: 8080, Destination: !Ref AppSG } # web -> app
AppSG:
Ingress:
- { Protocol: tcp, Port: 8080, Source: !Ref WebSG } # app accepts from web
Egress:
- { Protocol: tcp, Port: 5432, Destination: !Ref DbSG } # app -> db
DbSG:
Ingress:
- { Protocol: tcp, Port: 5432, Source: !Ref AppSG } # db accepts from app
Egress: [] # no egress at all
Adding a new app-tier instance simply adds the AppSG tag — the rules already cover it. CIDRs never come into the picture except at the edge (web-tier ingress).
7. Common Patterns
3-Tier Web App: public subnet (ALB + WebSG, only 443 from internet), private subnet (App tier, AppSG accepts only from WebSG), isolated subnet (RDS + DbSG, only 5432 from AppSG, no egress). NACLs match subnet purpose — isolated subnet NACL denies the IGW CIDR explicitly.
Bastion Host: small EC2 in a public subnet with BastionSG accepting SSH (22) only from your office IP set or VPN range. Workload SGs accept SSH only from BastionSG — no direct internet SSH anywhere. Better still: replace bastion with SSM Session Manager (no inbound port at all).
Zero-Trust Egress: private subnets have no NAT gateway. All outbound traffic goes through interface endpoints (for AWS services) or a centrally-managed proxy (Squid / Network Firewall) that allowlists destination domains. Workload SG egress is empty for AWS-only workloads.
Quarantine SG: a SG with no rules at all (no ingress, no egress). On a GuardDuty finding, an automated runbook detaches the workload's normal SG and attaches the quarantine SG — the instance is instantly cut off without termination, allowing forensics.
Cross-Account SG referencing: via VPC peering or Transit Gateway, you can reference an SG in a peered VPC by ARN — useful for shared-services architectures where the data tier lives in a separate account.
Defence-in-depth NACL: even when SGs are correct, NACLs block the IGW CIDR from the data subnet — so a misconfigured SG in that subnet still can't be reached from the internet.