Alright, let’s get into the weeds of the actual rules. This is where the rubber meets the road, and where most people, frankly, screw it up. Security Groups and NACLs don’t just magically allow traffic; you have to explicitly tell them what to permit or deny using a combination of three key elements: protocol, port range, and source/destination. Think of it as a very picky bouncer at an exclusive club. You have to tell him exactly who gets in (source), what kind of party they’re going to (port), and how they’re allowed to communicate (protocol).

The Protocol, Port, and Address Triad

Every single rule, whether it’s inbound or outbound, is built from these three components. They are non-negotiable.

  • Protocol: This is the type of traffic you’re dealing with. The usual suspects are TCP (for reliable, connection-oriented stuff like web traffic, SSH), UDP (for fast, connectionless stuff like DNS, video streaming), and ICMP (for the ping command, which is basically your network’s “you there?” message). You can also select “ALL,” which is a fantastic way to create a security nightmare if you’re not careful. We use protocols because networks speak different languages; TCP’s careful conversation is useless for understanding UDP’s frantic shouting.

  • Port Range: This specifies which application or service on your instance the rule applies to. Port 22 is for SSH, 80 for HTTP, 443 for HTTPS, etc. You can specify a single port (80), a range (8000-8080), or… well, you can just say “all ports.” I’ll wait here while you think about why that’s a catastrophically bad idea for most use cases.

  • Source (for Inbound) / Destination (for Outbound): This is the IP address (or range of addresses) that the traffic is coming from or going to. This is the “who” part of the rule. You can specify a single IP (192.0.2.10/32), a CIDR block (192.0.2.0/24), or… wait for it… 0.0.0.0/0 (which is the internet’s way of saying “absolutely everyone and their dog”). You can also reference another Security Group, which is a nifty AWS-specific feature we’ll get to.

Inbound vs. Outbound: A Critical Distinction

This is the single most important concept to internalize, and I see people get it wrong all the time.

  • Inbound Rules govern traffic coming to your resource (like an EC2 instance). The “source” is the origin of the traffic. If you want your web server to receive requests from the internet, you create an inbound rule allowing protocol TCP, port 80, source 0.0.0.0/0.

  • Outbound Rules govern traffic leaving from your resource. The “destination” is where the traffic is headed. Here’s the kicker: An inbound allow rule does NOT imply a corresponding outbound allow rule. They are evaluated independently. If your instance initiates a request to download a software update, that’s an outbound connection. The response from the update server comes back as inbound traffic, but it’s allowed because it’s a response to an established outbound connection (thanks to statefulness, which we’ll cover next). This is why your outbound rules are often wide open—you want your instance to be able to talk to the outside world. Locking down outbound traffic is for advanced, paranoid (often correct) scenarios.

Statefulness: The Secret Superpower of Security Groups

This is the genius part of Security Groups and why you’ll use them 95% of the time. Security Groups are stateful.

What does that mean? It means if you allow an outbound request, the return traffic for that request is automatically allowed, regardless of your inbound rules. You don’t need an explicit inbound rule to allow the response. It’s like the bouncer remembers who you let out and welcomes them back in without checking the list again.

NACLs, on the other hand, are stateless. If you allow an outbound request in a NACL, you must explicitly create an inbound rule to allow the return traffic. If you forget, your traffic gets silently dropped, and you’ll spend three hours pulling your hair out wondering why your internet connection doesn’t work. I’ve been there. It’s not fun.

Real-World Examples: The Good, The Bad, and The “You’ll Get Fired”

Let’s look at some practical rules. First, a sane, common Security Group for a web server:

{
  "GroupName": "web-server-sg",
  "GroupId": "sg-0a1b2c3d4e5f67890",
  "IpPermissions": [
    {
      "FromPort": 80,
      "IpProtocol": "tcp",
      "IpRanges": [
        {
          "CidrIp": "0.0.0.0/0"
        }
      ],
      "ToPort": 80,
      "UserIdGroupPairs": []
    },
    {
      "FromPort": 22,
      "IpProtocol": "tcp",
      "IpRanges": [
        {
          "CidrIp": "203.0.113.10/32" // Your office IP, NOT the whole internet!
        }
      ],
      "ToPort": 22,
      "UserIdGroupPairs": []
    }
  ]
}

You can create this easily with the AWS CLI. Here’s the command to authorize the SSH ingress rule from a specific IP:

aws ec2 authorize-security-group-ingress \
    --group-id sg-0a1b2c3d4e5f67890 \
    --protocol tcp \
    --port 22 \
    --cidr 203.0.113.10/32

Now, for the love of all that is holy, DO NOT DO THIS. This is the “please hack me” configuration:

{
  "IpPermissions": [
    {
      "FromPort": 0,
      "IpProtocol": "all",
      "IpRanges": [
        {
          "CidrIp": "0.0.0.0/0"
        }
      ],
      "ToPort": 65535,
      "UserIdGroupPairs": []
    }
  ]
}

This rule translates to: “Hey everyone on the internet, feel free to talk to any application on this instance using any protocol you want.” It completely negates the purpose of having a firewall.

Referencing Other Security Groups: The Cool Kid Feature

This is a brilliant AWS feature. Instead of using IP addresses, you can specify another Security Group as the source or destination. This creates a dynamic, intent-based security model.

Imagine you have an application server (App-SG) that needs to talk to a database (DB-SG). Instead of finding out the private IP of the app server (which might change) and hardcoding it into the DB-SG rules, you simply create an inbound rule on the DB-SG that allows traffic on port 5432 with a source of… the App-SG itself.

# On the DB Security Group (db-sg), allow PostgreSQL access from the app security group (app-sg)
aws ec2 authorize-security-group-ingress \
    --group-id sg-db123456 \
    --protocol tcp \
    --port 5432 \
    --source-group sg-app789abc

Now, any instance associated with app-sg can access the database. It’s self-assembling and incredibly elegant. This is how you build secure, scalable architectures without losing your mind managing IP lists. Always prefer this method over hardcoding IPs where possible. It’s what the designers got unequivocally right.