AWS / Guide 4

Security groups and network ACLs

How security groups and NACLs decide, referencing groups instead of IP addresses, risky rules, and checking what's allowed.

AWS has two network filters inside a VPC. Security groups sit on each network interface and do almost all of the work. Network ACLs sit on subnets and are a coarse second layer. Most exposure problems come from one security group rule that allows SSH or RDP from 0.0.0.0/0, added "just for today" and never removed.

Security groups and NACLs compared

Security groupNetwork ACL
Applies toNetwork interfaces (instances, load balancers, RDS, Lambda in a VPC...)Every resource in the subnets it's associated with
RulesAllow onlyAllow and deny
EvaluationAll rules together: if any rule allows, traffic is allowedIn number order; first match wins
StateStateful: replies are allowed automaticallyStateless: return traffic needs its own rule
DefaultNew group: no inbound, all outboundDefault NACL: allow everything both ways
Can referenceCIDRs, prefix lists and other security groupsCIDRs only
Quotas (default)60 inbound and 60 outbound rules; 5 groups per interface20 rules each way

Compared with Azure: a security group is closer to an NSG on a NIC combined with an application security group, and there's no priority number to manage. A NACL is closer to an NSG on a subnet, but stateless.

How security groups decide

  • There are no deny rules. Anything not allowed is denied. To block one address inside an allowed range, you need a NACL (or AWS Network Firewall).
  • Rules add up. An interface can have several security groups; the effective rules are the union of all of them. A permissive group attached "temporarily" overrides every careful one.
  • Changes apply immediately to new connections. Existing tracked connections usually continue until they close.
  • Outbound is open by default. New security groups allow all outbound traffic. Tighten it for sensitive tiers such as databases, which rarely need to start connections to anything.
  • The default security group in each VPC allows all traffic between its members. Don't use it: remove its rules (CIS benchmark item) and create named groups instead.

Reference security groups, not IP addresses

A security group rule can use another security group as its source. That's the AWS equivalent of Azure's application security groups, and it's the most useful habit to build:

GroupInbound ruleSource
sg-albTCP 4430.0.0.0/0 (or CloudFront's managed prefix list)
sg-webTCP 8443sg-alb
sg-appTCP 8080sg-web
sg-dbTCP 5432sg-app

Scale the web tier from two instances to twenty and no rule changes: anything with sg-web attached is allowed. Only the load balancer faces the internet; the instances don't need public IPs at all. Referencing works across peered VPCs in the same region and across VPCs attached to a Transit Gateway (when security group referencing is turned on for the attachment).

For groups of addresses outside AWS (office ranges, a partner), use a customer-managed prefix list: one list, referenced from many groups, updated in one place. AWS also publishes managed prefix lists for S3, DynamoDB and CloudFront origins.

$vpcId = (Get-EC2Vpc -Filter @{ Name = 'tag:Name'; Values = 'vpc-notes-test' }).VpcId

# The groups
$web = New-EC2SecurityGroup -VpcId $vpcId -GroupName 'sg-web' -Description 'Web tier'
$app = New-EC2SecurityGroup -VpcId $vpcId -GroupName 'sg-app' -Description 'App tier'

# sg-app accepts 8080 only from members of sg-web
$fromWeb = New-Object Amazon.EC2.Model.UserIdGroupPair -Property @{ GroupId = $web }
$rule = New-Object Amazon.EC2.Model.IpPermission -Property @{
    IpProtocol = 'tcp'; FromPort = 8080; ToPort = 8080; UserIdGroupPairs = @($fromWeb) }
Grant-EC2SecurityGroupIngress -GroupId $app -IpPermission $rule

# Attach sg-web to an instance's network interface (replaces the list, so include any it should keep)
$instance = (Get-EC2Instance -Filter @{ Name = 'tag:Name'; Values = 'ec2-notes-test-01' }).Instances[0]
Edit-EC2InstanceAttribute -InstanceId $instance.InstanceId -Group @($web)

Network ACLs as a second layer

Most VPCs run fine with the default allow-all NACL and good security groups. Use custom NACLs where a coarse, subnet-wide rule helps:

  • Blocking a specific address range quickly during an incident (security groups can't deny).
  • Separating tiers as a backstop: the data subnets only accept traffic from the app subnets, whatever the security groups say.

Because NACLs are stateless, every allow needs a matching rule for the reply, usually the ephemeral ports 1024–65535. Forgetting the outbound ephemeral rule is the classic cause of "the security group is right but it still doesn't work". Number rules in steps of 10 or 100 so you can insert one later.

Rules that cause trouble

These are the findings that come up in almost every review. Get-AwsSecurityGroupReport.ps1 looks for all of them across every region.

  • SSH (22) or RDP (3389) open to 0.0.0.0/0 or ::/0. Use Systems Manager Session Manager instead, which needs no inbound port at all (see Guide 5).
  • Database and remote-management ports open to the internet: 1433, 3306, 5432, 6379, 27017, 5985/5986 (WinRM) and similar.
  • All traffic (protocol -1) from anywhere. Usually added while troubleshooting. It turns the group off.
  • Forgotten IPv6. A rule tightened for 0.0.0.0/0 with a twin still open for ::/0.
  • Wide port ranges, such as 0-65535 or 3000-4000, which quietly include 3389.
  • Unused groups that nobody dares delete. Find-AwsOrphanedResources.ps1 lists groups not attached to any interface.

AWS Config's managed rules restricted-ssh, restricted-common-ports and vpc-default-security-group-closed, and the matching Security Hub controls, catch these continuously.

A naming and description convention

  • Name groups by role, not by instance: sg-<app>-<tier>, for example sg-payroll-web. A group named after one server ends up attached to ten.
  • Fill in the rule description on every rule (who asked, which ticket). It's the only place to record why a rule exists, and it shows in the console and in reports.
  • Manage groups in CloudFormation or Terraform. Drift detection then shows rules added by hand.

Check what is actually allowed

When a connection fails, don't read the rules and guess. Ask AWS:

  • Reachability Analyzer (VPC console) traces the path between two resources, through security groups, NACLs, route tables and gateways, and names the component that blocks it.
  • Network Access Analyzer finds unintended paths, for example "anything reachable from the internet gateway on port 22".
  • VPC flow logs record accepted and rejected connections for a VPC, subnet or interface. Send them to CloudWatch Logs for troubleshooting or to S3 for long-term, cheaper storage.
# Security groups on an instance, and their inbound rules
$instance.SecurityGroups | ForEach-Object { Get-EC2SecurityGroup -GroupId $_.GroupId } | ForEach-Object {
    $g = $_
    $g.IpPermissions | Select-Object @{n='Group';e={$g.GroupName}}, IpProtocol, FromPort, ToPort,
        @{n='From';e={ (@($_.Ipv4Ranges.CidrIp) + @($_.Ipv6Ranges.CidrIpv6) + @($_.UserIdGroupPairs.GroupId)) -join ', ' }}
}

# Turn on flow logs for the VPC, to CloudWatch Logs
New-EC2FlowLog -ResourceId $vpcId -ResourceType VPC -TrafficType ALL -LogDestinationType cloud-watch-logs `
    -LogGroupName '/vpc/vpc-notes-test/flow' -DeliverLogsPermissionArn 'arn:aws:iam::111122223333:role/vpc-flow-logs'

Examples to try

From aws.ravindran.in: VPC security groups, network ACLs, VPC basics and VPC peering.