Skip to content

VPC Security groups should only allow unrestricted incoming traffic for authorized ports

Security groups are the primary instance-level firewall in AWS. When a rule allows 0.0.0.0/0 on ports like 22, 3389, or 3306, you expose management interfaces and databases to the entire internet. Attackers actively scan for these ports, and a single misconfigured rule can lead to credential brute-forcing or direct data exfiltration.

Limiting unrestricted ingress to known web-serving ports (80, 443) forces teams to use bastion hosts, VPNs, or AWS Systems Manager Session Manager for administrative access. That significantly reduces the blast radius when a security group is misconfigured.

Retrofit consideration

Existing security groups with unrestricted ingress on non-authorized ports (e.g., 22 for SSH bastion hosts) will fail immediately. You must either restrict the source CIDR, migrate to Session Manager, or add the port to the authorized list if business-justified. Audit all groups with aws ec2 describe-security-groups --filters Name=ip-permission.cidr,Values=0.0.0.0/0 before enforcing.

Implementation

Choose the approach that matches how you manage Terraform.

Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.

module "ec2_instance" {
  source  = "pcidss.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "nist80053.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "fedrampmoderate.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "cisv80ig1.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "nist800171.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "nydfs23.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "cccsmedium.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "iso270012013.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

module "ec2_instance" {
  source  = "pcidssv321.compliance.tf/terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

If you use terraform-aws-modules/ec2-instance/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.

module "ec2_instance" {
  source  = "terraform-aws-modules/ec2-instance/aws"
  version = ">=6.0.0"

  ami_ssm_parameter = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64"
  instance_type     = "t4g.nano"
  subnet_id         = "subnet-abc123"

  security_group_ingress_rules = {
    from_port = 80
  }
}

Use AWS provider resources directly. See docs for the resources involved: aws_vpc_security_group_ingress_rule.

resource "aws_vpc_security_group_ingress_rule" "this" {
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "tcp"
  security_group_id = "sg-abc12345"
  to_port           = 443

  from_port = 80
}

What this control checks

This control evaluates aws_vpc_security_group_ingress_rule resources (or the older aws_security_group_rule with type = "ingress"). A rule fails when cidr_ipv4 is "0.0.0.0/0" or cidr_ipv6 is "::/0" and the from_port/to_port range covers any port outside the authorized list. To pass, either restrict the source CIDR to a specific range (e.g., "10.0.0.0/8") or ensure the port matches an authorized value. For the default configuration, only rules targeting exactly port 80 or 443 with a world-open CIDR pass. A rule with from_port = 0 and to_port = 65535 always fails because it covers every unauthorized port. If your organization needs additional authorized ports (e.g., 8443 for an API gateway), update the control parameter authorizedTcpPorts to include them.

Common pitfalls

  • Inline ingress blocks can complicate policy checks

    Policy enforcement against the ingress block inside aws_security_group is less consistent than against standalone aws_vpc_security_group_ingress_rule resources. AWS Config catches the violation at runtime either way, but Terraform-level checks may miss inline blocks depending on tool support. Migrate to standalone rule resources to get reliable shift-left enforcement.

  • Port ranges that span authorized and unauthorized ports

    A rule with from_port = 80 and to_port = 90 silently opens ports 81 through 90 in addition to 80. The control flags the entire range because it includes unauthorized ports. Always use exact single-port rules (from_port = 80, to_port = 80) rather than ranges that happen to include an authorized port.

  • IPv6 CIDR often overlooked

    Lock down cidr_ipv4 and forget cidr_ipv6 = "::/0" and you still have unrestricted internet access on IPv6-enabled VPCs. Both address families are evaluated independently. If your VPC has IPv6 enabled, audit both arguments on every ingress rule.

  • Protocol -1 (all traffic) always fails

    Setting ip_protocol = "-1" on an aws_vpc_security_group_ingress_rule with a world-open CIDR allows all ports and protocols. There is no authorized-ports check that can save it because the rule implicitly includes every unauthorized port. Replace it with explicit protocol and port rules.

  • Default security group with inherited rules

    Get this wrong and untracked 0.0.0.0/0 rules accumulate in the default security group outside Terraform state, with nothing to flag them until an audit. Import the default security group using aws_default_security_group and manage its rules explicitly so any ingress added outside Terraform shows up as drift.

Audit evidence

Auditors expect Config rule evaluation results for the managed rule vpc-sg-open-only-to-authorized-ports showing all security groups as COMPLIANT. A supplementary report listing each security group with unrestricted ingress, the associated ports, and the business justification for any non-default authorized ports rounds out the documentation package. CloudTrail AuthorizeSecurityGroupIngress events show that new rules were reviewed and approved before taking effect. Screenshots from the VPC console covering inbound rules for security groups attached to public-facing instances confirm no unauthorized open ports are present.

Framework-specific interpretation

PCI DSS v4.0: Requirement 1.3 allows only necessary and authorized inbound communications to the cardholder data environment. An open port on a CDE security group is exactly the unnecessary exposure Requirement 1.3 is designed to prevent, and examiners will ask for evidence that every unrestricted rule has a documented justification.

NIST SP 800-53 Rev 5: SC-7(5) requires deny by default, allow by exception, and AC-4 extends that to information flow between security domains. This control operationalizes both at the security group level: any world-open rule on a non-approved port fails the documented authorization requirement NIST 800-53 Rev 5 expects.

FedRAMP Moderate Baseline Rev 4: SC-7 (Boundary Protection) and AC-4 (Information Flow Enforcement) both apply here. At the Moderate baseline, network boundaries must enforce explicit allow-lists, not default-open rules. An unrestricted ingress rule on an unauthorized port is a direct SC-7 violation regardless of what sits behind it.

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: vpc_security_group_allows_ingress_authorized_ports

  • AWS Config Managed Rule: VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS

  • Powerpipe Control: aws_compliance.control.vpc_security_group_allows_ingress_authorized_ports

  • AWS Security Hub Control: EC2.18

Last reviewed: 2026-03-09