Back to blog
AWSBest PracticesCloud SecurityNetworkingOperations & Compliance

Security Group Not Attached: Cleaning Up Orphaned AWS Security Groups

Learn why unattached AWS security groups create audit noise and hidden risk, plus step-by-step CLI, console, and IaC fixes to clean them up and prevent recurrence.

TL;DR

This check flags AWS security groups that are not attached to any network interface. They clutter your account, slow down audits, and often hide stale rules that drift into your defaults. Review each unused group, then delete the ones nobody owns with aws ec2 delete-security-group.

Security groups pile up. Someone spins up a test instance, attaches a custom group, tears the instance down, and the group stays behind. Multiply that across a few years and a few dozen engineers, and you end up with hundreds of orphaned groups that nobody can explain. The Lensix Security Group Not Attached check (sg_unused) finds these so you can clean house before they cause real problems.


What this check detects

The check scans every security group in a region and looks at whether it is referenced by an elastic network interface (ENI). A security group only does anything when it is attached to an ENI, which is what backs EC2 instances, load balancers, RDS databases, Lambda functions in a VPC, and similar resources. If no ENI references the group, it is effectively inert.

When Lensix finds a group with zero ENI attachments, it raises sg_unused. The default VPC security group is treated as a special case because it always exists and cannot be deleted, but a custom group with no attachments is fair game for cleanup.

Note: A security group can also be referenced by other security groups (for example, a rule that allows traffic from sg-abc123). Lensix accounts for this. A group that is not attached to an ENI but is still referenced as a source in another group's rules is a separate situation, since deleting it would break those rules.


Why it matters

An unused security group is not an active vulnerability by itself. The problem is what it does to your operational hygiene and your audit surface over time.

It hides risk in plain sight

Orphaned groups often carry old rules: 0.0.0.0/0 on port 22, a database port open to a wide CIDR, or a forgotten allow rule for a vendor that no longer exists. As long as the group sits unattached, those rules do nothing. But the moment someone reaches for "an existing group" to save time, they can re-attach all of that exposure to a live resource without realizing it.

It slows down audits and incident response

During a security review or a breach investigation, you want to reason about your actual network exposure quickly. Hundreds of dead groups force you to filter signal from noise. Every group an analyst has to inspect and dismiss is wasted time, and wasted time during an incident has a real cost.

It pushes you toward AWS limits

AWS enforces quotas on security groups per VPC and rules per group. Accounts that never clean up can bump into these limits, which then blocks legitimate deployments until someone scrambles to figure out what is safe to delete.

Warning: Do not assume an unattached group is safe to delete just because no ENI uses it right now. Some teams keep "template" groups on purpose, and some groups are referenced as a source in other groups' rules. Confirm both before you remove anything.


How to fix it

The fix is to identify each unused group, confirm nobody depends on it, and delete it. Work through these steps.

1. List security groups with no attachments

First, get every group ID in the region, then cross-reference against the groups currently in use by ENIs.

# All security group IDs in the region
aws ec2 describe-security-groups \
  --query 'SecurityGroups[].GroupId' \
  --output text | tr '\t' '\n' | sort > all_sgs.txt

# Security group IDs currently attached to an ENI
aws ec2 describe-network-interfaces \
  --query 'NetworkInterfaces[].Groups[].GroupId' \
  --output text | tr '\t' '\n' | sort -u > used_sgs.txt

# The difference is your candidate list of unused groups
comm -23 all_sgs.txt used_sgs.txt

2. Check whether anything references the group

Before deleting, make sure no other security group uses the candidate as a source or destination in its rules.

# Replace sg-0123456789abcdef0 with your candidate
aws ec2 describe-security-groups \
  --filters Name=ip-permission.group-id,Values=sg-0123456789abcdef0 \
  --query 'SecurityGroups[].GroupId' \
  --output text

If that command returns any group IDs, the candidate is referenced by those groups. Update or remove those references first, otherwise the delete will fail.

3. Inspect the rules before you delete

Take a quick look at what the group actually allowed. This is useful both for a paper trail and for spotting rules you may want to preserve elsewhere.

aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0 \
  --query 'SecurityGroups[0].{Name:GroupName,Desc:Description,Ingress:IpPermissions,Egress:IpPermissionsEgress}'

4. Delete the group

Danger: Deleting a security group is irreversible. If the group is later attached to a resource through automation you missed, that deployment will fail. Confirm the group is unattached and unreferenced, and ideally export its rules to a JSON file first so you can recreate it if needed.

# Export the group definition for safekeeping
aws ec2 describe-security-groups \
  --group-ids sg-0123456789abcdef0 > backup-sg-0123456789abcdef0.json

# Delete it
aws ec2 delete-security-group --group-id sg-0123456789abcdef0

If you get a DependencyViolation error, the group is still referenced somewhere. Go back to step 2 and clear those references before retrying.

Tip: If you have dozens of candidates, wrap the delete in a loop with a dry-run guard. Pipe your reviewed list into a small script that prints the group name and rule count, asks for confirmation, then deletes. A few minutes of scripting beats clicking through the console one group at a time.

Console alternative

  1. Open the EC2 console and go to Network & Security > Security Groups.
  2. Select a group and open the Network interfaces tab on the details pane, or check the linked ENIs.
  3. If no interfaces are listed and no other group references it, select the group and choose Actions > Delete security groups.

How to prevent it from happening again

Cleanup is a one-time win. Prevention is what keeps your account from filling back up.

Tag groups with an owner and purpose

Require a small set of tags on every security group so future-you knows who to ask before deleting. A group with owner and purpose tags is far easier to retire confidently.

aws ec2 create-tags \
  --resources sg-0123456789abcdef0 \
  --tags Key=owner,Value=platform-team Key=purpose,Value=legacy-bastion

Manage groups as code

When security groups live in Terraform or CloudFormation, an unused group is one that no longer has a resource referencing it, which shows up clearly in a plan or diff. Avoid creating groups by hand in the console, since those are the ones that go orphaned.

resource "aws_security_group" "app" {
  name        = "app-tier"
  description = "App tier ingress from ALB only"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  tags = {
    owner   = "platform-team"
    purpose = "app-tier"
  }
}

Run the check on a schedule

Schedule sg_unused to run continuously in Lensix and route findings to your team's channel. Catching three orphaned groups a week is painless. Catching three hundred at audit time is a project.

Add a CI/CD gate

If you use Terraform, fail a pull request that introduces a group with no references, or use a policy-as-code tool to flag overly broad rules before they merge. A simple OPA/Conftest policy can enforce that any new security group includes the required tags.

package terraform.sg

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group"
  not resource.change.after.tags.owner
  msg := sprintf("security group %s is missing an 'owner' tag", [resource.address])
}

Tip: Pair this check with a broader review of overly permissive ingress rules. Unused groups and open 0.0.0.0/0 rules tend to come from the same habit of clicking through the console under time pressure.


Best practices

  • Treat groups as disposable, not permanent. Create them with code, attach them deliberately, and remove them when the resource goes away.
  • Tag everything. An owner tag turns "is this safe to delete?" from a research project into a one-message Slack question.
  • Audit references, not just attachments. A group with no ENI can still be load-bearing if other groups point to it.
  • Keep a short retention window. Run the unused-group check weekly and act on it, rather than letting findings accumulate.
  • Back up before deleting. Export the group definition to JSON so you can recreate it if a forgotten dependency surfaces.
  • Watch your quotas. Set an alert before you approach the security-groups-per-VPC limit, so cleanup happens on your schedule and not during an outage.

Unused security groups are low-drama on a quiet day and a real headache during an audit or incident. Clear them out, automate the detection, and keep your network surface small enough to reason about.