We regularly encounter projects where misconfigured Security Groups or NACLs lead to data leaks or service unavailability. A common issue is leaving SSH (port 22) open to all IPs after testing. Or forgotten ephemeral ports in NACLs blocking return traffic. Let's look at how to properly organize network security in the cloud. Over the past years, we have audited over 50 cloud infrastructures: in 70% of cases we found critical errors in firewall rules. We'll tell you how to avoid these mistakes.
Why Security Groups Don't Replace NACLs
Security Groups and Network ACLs are different layers of protection in AWS that are often confused. Security Group operates at the ENI (network interface) level, is stateful — return traffic is automatically allowed. NACL operates at the subnet level, is stateless — rules must be written explicitly for both directions. Most issues with "why is it not accessible" in AWS are due to incorrectly configured NACLs. Understanding the difference is critical: according to statistics, 80% of incidents are related to incorrect NACL configuration.
Key differences are summarized in the table:
| Parameter | Security Group | NACL |
|---|---|---|
| Level | Instance (ENI) | Subnet |
| Stateful | Yes | No |
| Deny support | No (only allow) | Yes |
| Rule order | All rules are evaluated | By number (first match) |
| Typical use | Application access control | Blocking suspicious IPs, extra barrier |
According to AWS documentation, Security Group is the first barrier, NACL is the second. However, in practice many limit themselves to Security Groups only.
How to Automate Rule Management with Terraform?
Using Infrastructure as Code (Terraform) speeds up changes by 3 times compared to manual configuration via the console. Terraform code allows versioning rules and performing code reviews.
Example of a correct Security Group structure:
resource "aws_security_group" "web" {
name = "web-tier"
description = "HTTP/HTTPS from internet"
vpc_id = var.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
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" "app" {
name = "app-tier"
description = "Traffic from web tier only"
vpc_id = var.vpc_id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id] # reference to SG, not CIDR
}
}The reference security_groups = [sg-id] instead of CIDR is a key pattern. When scaling the web-tier, new instances automatically gain access to the app-tier without manual rule updates.
NACL: When You Can't Do Without Them
NACLs are needed in two cases: explicit deny for specific IPs (Security Group cannot deny — only allow), and an additional barrier at the subnet level.
Example Terraform code for NACL:
resource "aws_network_acl" "private" {
vpc_id = var.vpc_id
subnet_ids = var.private_subnet_ids
# Explicitly block suspicious IP
ingress {
rule_no = 90
protocol = "-1"
action = "deny"
cidr_block = "198.51.100.0/24"
from_port = 0
to_port = 0
}
# Allow from VPC
ingress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = var.vpc_cidr
from_port = 0
to_port = 65535
}
# Ephemeral ports for stateless NACL (mandatory!)
egress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
} Common mistake: ephemeral ports
Ephemeral ports (1024–65535) in egress NACL rules are what is most often forgotten. Return traffic for TCP requests comes from ephemeral ports. Without this rule, the NACL blocks responses, even if the Security Group allows everything normally.Comparison of Cloud Providers
| Cloud | Main Object | Stateful/Stateless | Level |
|---|---|---|---|
| AWS | Security Group, NACL | SG stateful, NACL stateless | Instance/Subnet |
| GCP | Firewall Rules | Stateful | VPC |
| Azure | NSG | Stateful | Subnet/NIC |
In GCP there is no NACL — everything is done through Firewall Rules at the VPC level. An important feature: rules are applied by instance tags or service accounts, not by subnet:
resource "google_compute_firewall" "allow_internal_app" {
name = "allow-app-from-web"
network = var.network_name
allow {
protocol = "tcp"
ports = ["8080"]
}
source_tags = ["web-tier"]
target_tags = ["app-tier"]
}
resource "google_compute_firewall" "deny_all_ingress" {
name = "deny-all-ingress"
network = var.network_name
direction = "INGRESS"
priority = 65534
deny {
protocol = "all"
}
source_ranges = ["0.0.0.0/0"]
}In Azure, NSG can be attached to either a subnet or directly to a NIC. Augmentation with service tags (AzureLoadBalancer, VirtualNetwork) simplifies rules:
resource "azurerm_network_security_rule" "allow_lb" {
name = "allow-azure-lb"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
resource_group_name = var.rg_name
network_security_group_name = azurerm_network_security_group.app.name
} Typical Configuration Mistakes
- Open port 0–65535 for
0.0.0.0/0in outbound Security Group — encountered almost everywhere. Theoretically allows any outbound connections from the instance, including data exfiltration. In practice, outbound is restricted only where justified by management complexity. - Duplicate rules in NACL with different
rule_no— NACL processes rules by number, first match applies. Rule 90 with deny overrides rule 100 with allow for the same CIDR. - Using CIDR instead of Security Group references — requires manual updates when scaling.
What's Included in the Work
- Audit of current rules — identify unused, redundant, and dangerous rules. Provide a detailed report with recommendations.
- Design of new configuration — use Terraform to declaratively describe all Security Groups and NACLs. All changes go through code review.
- Documentation — network diagram with all rules, description of exceptions and justifications.
- Team training — how to maintain rules via IaC, what to do when adding a new microservice.
Work Process
- Analytics — collect current network topology, firewall rules, compliance requirements. Identify critical vulnerabilities.
- Design — develop target architecture using least privilege principle. Determine which rules can be consolidated, where NACLs are needed.
- Implementation — write Terraform code, create pull request, conduct code review. Deploy first in staging environment.
- Testing — verify connectivity between services, ensure legitimate traffic is not blocked. Use test scenarios.
- Production deployment — apply changes on schedule, monitor errors via CloudWatch / Stackdriver.
Timelines
Audit and configuration of Security Groups for a typical three-tier application (web/app/db) in one cloud — 1-2 days. Multi-cloud or complex microservice architecture with dozens of Security Groups — 3-5 days. Investment in audit and configuration typically pays for itself by preventing incidents: the average cost of a data breach is $4.35 million according to IBM. Cost is calculated individually after analyzing the current architecture.
Over the years, we have completed more than 50 cloud security projects. Certified AWS and Azure engineers ensure your network is secured according to best practices. Get an analysis of your current security rules. Contact us for an audit of your cloud infrastructure. Request a consultation with a DevOps engineer now.







