Modern QA2026Network Rule Verification — tiles
Log inJoin
60 / 75 · 08 Infrastructure as Code Testing · IAM Policy and Network Rule Verification← prev⊞ allnext →☰ Read as one page

11.3Network Rule Verification

Security Group Testing with Terraform Plan

# tests/test_network_rules.py
import json
import pytest

@pytest.fixture
def tfplan():
    """Load Terraform plan JSON."""
    with open("tfplan.json") as f:
        return json.load(f)

def test_no_public_ingress_on_database_sg(tfplan):
    """Database security groups must not allow ingress from 0.0.0.0/0."""
    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_security_group":
            if "database" in change["address"] or "rds" in change["address"]:
                ingress_rules = change["change"]["after"].get("ingress", [])
                for rule in ingress_rules:
                    cidrs = rule.get("cidr_blocks", [])
                    assert "0.0.0.0/0" not in cidrs, \
                        f"Database SG {change['address']} allows public ingress"
                    assert "::/0" not in cidrs, \
                        f"Database SG {change['address']} allows public IPv6 ingress"

def test_database_sg_only_allows_specific_ports(tfplan):
    """Database security groups should only allow database ports."""
    allowed_ports = {5432, 3306, 27017, 6379}  # Postgres, MySQL, Mongo, Redis

    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_security_group":
            if "database" in change["address"] or "rds" in change["address"]:
                ingress_rules = change["change"]["after"].get("ingress", [])
                for rule in ingress_rules:
                    from_port = rule.get("from_port", 0)
                    to_port = rule.get("to_port", 0)
                    assert from_port in allowed_ports, \
                        f"Database SG allows unexpected port {from_port}"
                    assert from_port == to_port, \
                        f"Database SG has port range {from_port}-{to_port}"

def test_no_unrestricted_egress(tfplan):
    """Security groups should not have unrestricted egress to the internet."""
    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_security_group":
            egress_rules = change["change"]["after"].get("egress", [])
            for rule in egress_rules:
                if "0.0.0.0/0" in rule.get("cidr_blocks", []):
                    # If egress is open, it must not be on all ports
                    assert rule.get("from_port", 0) != 0 or \
                           rule.get("to_port", 0) != 65535, \
                        f"SG {change['address']} has unrestricted egress on all ports"

def test_ssh_restricted_to_vpn(tfplan):
    """SSH access (port 22) must be restricted to VPN CIDR blocks only."""
    vpn_cidrs = ["10.0.0.0/8", "172.16.0.0/12"]

    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_security_group_rule":
            after = change["change"]["after"]
            if after.get("from_port") == 22 and after.get("type") == "ingress":
                cidrs = after.get("cidr_blocks", [])
                for cidr in cidrs:
                    assert cidr in vpn_cidrs, \
                        f"SSH rule in {change['address']} allows access from {cidr}"

VPC Configuration Testing

def test_private_subnets_have_no_public_ips(tfplan):
    """Private subnets must not auto-assign public IPs."""
    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_subnet":
            if "private" in change["address"]:
                map_public = change["change"]["after"].get(
                    "map_public_ip_on_launch", False
                )
                assert map_public is False, \
                    f"Private subnet {change['address']} assigns public IPs"

def test_nacl_denies_known_bad_ports(tfplan):
    """Network ACLs should deny traffic on known dangerous ports."""
    dangerous_ports = [23, 135, 139, 445, 1433, 3389]  # Telnet, RPC, SMB, MSSQL, RDP

    for change in tfplan["resource_changes"]:
        if change["type"] == "aws_network_acl_rule":
            after = change["change"]["after"]
            if after.get("rule_action") == "allow":
                from_port = after.get("from_port", 0)
                to_port = after.get("to_port", 0)
                for port in dangerous_ports:
                    if from_port <= port <= to_port:
                        pytest.fail(
                            f"NACL rule {change['address']} allows "
                            f"dangerous port {port}"
                        )