Back to Checks
aws

Security group does not allow ingress from 0.0.0.0/0 or ::/0 to MySQL port 3306

ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306
Service: ec2Severity: high
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:AwsEc2SecurityGroup

EC2 security groups are assessed for inbound exposure of MySQL on TCP 3306 from 0.0.0.0/0 or ::/0.

The finding reflects whether this port is reachable from any IPv4 or IPv6 address.

Risk

Public MySQL access lets anyone reach the service, enabling credential brute force and vulnerability exploitation. This threatens:

  • Confidentiality: data exfiltration
  • Integrity: unauthorized writes or schema changes
  • Availability: DoS from abuse or scans

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306

Recommendation

Apply least privilege: restrict 3306 to specific sources or peer security groups only. Keep databases in private subnets and use VPN, bastion, or application proxies for admin access. Enable defense in depth with TLS and strong auth. Never allow 0.0.0.0/0 or ::/0 ingress.

Remediation

Terraform

# Restrict MySQL (3306) from Internet
resource "aws_security_group" "example_resource_name" {
  vpc_id = "<example_resource_id>"

  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"] # Critical: not 0.0.0.0/0 or ::/0; restricts MySQL access
  }
}

NativeIaC

# CloudFormation: restrict MySQL (3306) from Internet
Resources:
  ExampleSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Limit MySQL access
      VpcId: <example_resource_id>
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 10.0.0.0/8  # Critical: not 0.0.0.0/0 or ::/0; limits MySQL to internal range

Other

1In AWS Console, go to EC2 > Security Groups
2Select the security group in use by the instance
3In Inbound rules, click Edit inbound rules
4Remove any rule for TCP port 3306 with source 0.0.0.0/0 or ::/0
5Add a rule for TCP 3306 only from a trusted source (e.g., specific IP/CIDR or a security group)
6Click Save rules

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
from prowler.providers.aws.services.ec2.ec2_securitygroup_allow_ingress_from_internet_to_all_ports import (
    ec2_securitygroup_allow_ingress_from_internet_to_all_ports,
)
from prowler.providers.aws.services.ec2.lib.security_groups import check_security_group
from prowler.providers.aws.services.vpc.vpc_client import vpc_client


class ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306(Check):
    def execute(self):
        findings = []
        check_ports = [3306]
        for security_group_arn, security_group in ec2_client.security_groups.items():
            # Check if ignoring flag is set and if the VPC and the SG is in use
            if ec2_client.provider.scan_unused_services or (
                security_group.vpc_id in vpc_client.vpcs
                and vpc_client.vpcs[security_group.vpc_id].in_use
                and len(security_group.network_interfaces) > 0
            ):
                report = Check_Report_AWS(
                    metadata=self.metadata(), resource=security_group
                )
                report.resource_details = security_group.name
                report.status = "PASS"
                report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have MySQL port 3306 open to the Internet."
                # only proceed if check "..._to_all_ports" did not run or did not FAIL to avoid to report open ports twice
                if not ec2_client.is_failed_check(
                    ec2_securitygroup_allow_ingress_from_internet_to_all_ports.__name__,
                    security_group_arn,
                ):
                    # Loop through every security group's ingress rule and check it
                    for ingress_rule in security_group.ingress_rules:
                        if check_security_group(
                            ingress_rule, "tcp", check_ports, any_address=True
                        ):
                            report.status = "FAIL"
                            report.status_extended = f"Security group {security_group.name} ({security_group.id}) has MySQL port 3306 open to the Internet."
                            report.resource_details = security_group.name
                            report.resource_id = security_group.id
                            break
                else:
                    report.status_extended = f"Security group {security_group.name} ({security_group.id}) has all ports open to the Internet and therefore was not checked against the specific MySQL port 3306."

                findings.append(report)

        return findings