Right, let’s talk about one of AWS’s more elegant features that they somehow managed to make feel clunky: allowing one security group to talk to another. It’s the networking equivalent of saying, “My friend here is cool, let him in,” instead of having to check his ID every single time. We call this a security group reference.

The core idea is beautifully simple. Instead of specifying a CIDR block (like 10.0.0.0/16) as the source in your security group’s inbound rule, you specify another security group’s ID (like sg-0a1b2c3d4e5f67890). This creates a dynamic, logical rule: “Allow traffic from any network interface that is currently attached to the source security group.”

Why would you bother? Imagine a classic two-tier application: web servers in one Auto Scaling group and databases in another. Your web servers need to talk to your databases on port 5432 (PostgreSQL). The old-school, manual way would be to find the CIDR block of your web tier’s VPC subnet (say, 10.0.1.0/24) and create an inbound rule on your DB security group for that. But what if your web tier scales into another subnet, 10.0.2.0/24? Now you have to remember to add that CIDR block to your DB security group. It’s a maintenance nightmare waiting to happen.

The security group reference solves this. You create a security group for your web servers (WebServerSG) and one for your databases (DBSG). On the DBSG, you create an inbound rule that allows TCP port 5432, but instead of a CIDR, you use WebServerSG as the source. Now, any EC2 instance that gets launched and attached to WebServerSG is automatically granted access, regardless of its private IP address or what subnet it lands in. It’s self-updating, elastic, and frankly, the way you should always do this.

The Nitty-Gritty: How It Actually Works

Don’t let the simplicity fool you; the devil’s in the details. The rule isn’t “allow traffic from instances using the source SG.” It’s more precise: “allow traffic from the primary network interface (eth0) that has the source SG attached to it.” This is a crucial distinction.

This means if an instance has multiple network interfaces (ENIs), traffic coming from its secondary ENI (eth1) won’t be allowed by a rule that references the SG attached to its primary ENI (eth0). The rule is tied to the network interface’s membership, not the instance’s identity. It’s a bit pedantic, but it matters for complex networking setups.

Here’s what a Terraform configuration for this looks like. Notice how the db_security_group uses the web_security_group.id as its source, not a CIDR block.

resource "aws_security_group" "web_security_group" {
  name_prefix = "webserver-sg-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "db_security_group" {
  name_prefix = "database-sg-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.web_security_group.id] # This is the magic
  }

  # ... egress rules for the DB to talk to the world if needed
}

The Classic “Circular Reference” Pitfall

This is where everyone gets bitten. Let’s say you have SG A and SG B. You want them to talk to each other. So, on SG A, you add an inbound rule with SG B as the source. On SG B, you add an inbound rule with SG A as the source. You’ve just created a perfect logical loop. This is often done by accident when setting up rules for application health checks or communication between peered VPCs.

The result? Nothing breaks, but your security posture is now Swiss cheese. You’ve effectively allowed all traffic between every instance in SG A and every instance in SG B, on all ports. Why? Because the evaluation is stateful and logical. An instance in SG A is allowed to talk to SG B because of the rule on SG B. That instance’s return traffic from SG B is automatically allowed because of the stateful nature of security groups. It’s a permissions feedback loop. Always, always audit your security groups for these circular dependencies. I’ve seen this hide in production environments for years.

The Peering Quandary

Here’s a fun one that feels like an AWS design oversight. You cannot reference a security group across a VPC peering connection. It just doesn’t work. The console might even show you the security group from the peered VPC, tempting you to select it, but if you try, you’ll get a classic AWS error message that essentially translates to “lol no.”

The official, committee-written reason is something about preventing accidental dependencies and ownership issues. My more direct, trench-based reason is that it’s a pain in the neck to implement and they haven’t gotten around to it. So what’s the solution? You’re thrown back to the dark ages: you must use CIDR blocks. You have to reference the CIDR block of the peered VPC or, better yet, the specific subnet CIDRs where your source instances live. It’s less dynamic and more fragile, but it’s the only game in town for cross-VPC traffic. Plan your CIDR blocks accordingly!