Mohammad Gufran Jahangir January 19, 2026 0

If you’ve ever stared at a “connection timed out” error and thought, “Is it DNS? Is it routing? Is it security groups? Is it the firewall?” — you’re not alone.

Network security in cloud and modern datacenters is confusing because it’s layered. You can have multiple doors that can block the same packet:

  • A security group (or similar) at the workload level
  • A NACL (or similar) at the subnet boundary
  • A firewall appliance (or corporate firewall) in the network path
  • Sometimes a WAF or service mesh policy on top of that

And here’s the trick:

A connection only works if every layer allows it.
A connection fails if any one layer blocks it.

This blog will make it click — even if you’re a beginner — and give you practical patterns you can reuse for years.


Table of Contents

The fastest mental model: “Bouncer, Gate, and Border Patrol”

Think of your network like a building:

1) Security Groups (SG) = The bouncer at the door

  • Applied close to the workload (server/VM/pod/ENI)
  • Controls who can talk to that thing
  • Usually stateful (more on that soon)
  • Great for micro-segmentation and “only this app can talk to that DB”

2) Network ACLs (NACL) = The gate at the neighborhood

  • Applied at the subnet boundary
  • Controls what can enter/leave the subnet
  • Usually stateless
  • Great for broad guardrails like “this subnet should never accept inbound SSH from the internet”

3) Firewalls = The border patrol

  • Central inspection and enforcement across networks
  • Often includes advanced features: IDS/IPS, domain filtering, threat intelligence, NAT, TLS inspection (in some environments)
  • Great for organization-wide rules like “only approved egress destinations,” “block known bad IPs,” or “force all outbound traffic through inspection”

If you remember just one line, remember this:

SGs protect workloads. NACLs protect subnets. Firewalls protect networks.

Now let’s make the differences crystal clear.


The single most important concept: Stateful vs Stateless

Stateful (Security Groups are usually stateful)

Stateful means:

If inbound is allowed, the return traffic is automatically allowed.

Example:

  • You allow inbound HTTPS (443) to a web server.
  • When the server replies back to the client, you don’t need to add an explicit outbound rule for that response (the state engine remembers the connection).

Why it matters: Stateful is easier and safer for app-level controls.


Stateless (NACLs are usually stateless)

Stateless means:

You must allow traffic in both directions explicitly.

Example:

  • Client → Server on 443
  • Server → Client response uses ephemeral ports (usually high ports like 1024–65535)
  • With a stateless filter, you must permit:
    • inbound 443 to the server subnet
    • outbound ephemeral ports back to the client subnet (or vice versa depending on direction)

Why it matters: Stateless rules are stricter but easier to break accidentally.


Why “ephemeral ports” cause 80% of NACL confusion

A “connection” is not just one port.

When you browse a website:

  • You connect to server port 443
  • Your client uses a random high port (ephemeral) like 53122
  • So the flow looks like:
  1. Client 53122 → Server 443
  2. Server 443 → Client 53122

If your subnet ACL allows only port 443 and nothing else, the response might be blocked, because the response goes to an ephemeral port on the client side.

Rule of thumb:

  • For stateless controls, you almost always need to allow ephemeral port ranges for return traffic.

Quick cheat sheet: SG vs NACL vs Firewall

Security Group (SG)

Best for:

  • “Only app A can talk to DB on 5432”
  • “Only load balancer can talk to app on 8080”
  • “Allow SSH only from bastion”

Common traits:

  • Applied to instances/workloads
  • Stateful
  • Allow rules (often “deny” is implicit)

NACL

Best for:

  • “No inbound SSH/RDP from internet for this subnet”
  • “Block specific IP ranges at subnet border”
  • “Protect against accidental exposure at subnet level”

Common traits:

  • Applied to subnets
  • Stateless
  • Has explicit allow and deny
  • Rule order matters

Firewall appliance / centralized firewall

Best for:

  • “All outbound internet access must go through inspection”
  • “Block known malicious destinations”
  • “Only allow egress to approved SaaS domains/IPs”
  • “Segment entire environments (prod vs dev) across networks/accounts/projects”

Common traits:

  • Central point
  • Deep inspection options
  • Often used in hub-and-spoke architectures

The “least privilege ladder” (how pros design it)

A strong design doesn’t rely on a single control. It layers controls from broad to specific:

  1. Firewall (broad org rules)
  2. NACL (subnet guardrails)
  3. Security Groups (precise workload rules)
  4. Optional: WAF / service mesh / app auth (layer 7 controls)

But layering doesn’t mean “more rules everywhere.”
It means each layer has a job and you avoid duplicating rules mindlessly.


Step-by-step: Build a secure 3-tier network using patterns

Let’s build a classic system:

  • Public tier: Load Balancer
  • App tier: Application servers / Kubernetes workers
  • Data tier: Database (Postgres/MySQL)

Desired flows

  • Internet → Load Balancer (443)
  • Load Balancer → App (8080 or 443)
  • App → DB (5432)
  • Admin access via Bastion or VPN only
  • Outbound internet restricted (optional but recommended)

Pattern 1: “Only the load balancer can talk to the app”

Security group rules (practical)

SG-LB (Load Balancer)

  • Inbound: 443 from 0.0.0.0/0 (or from corporate IPs if internal)
  • Outbound: to App tier on 8080 (or 443)

SG-App

  • Inbound: 8080 from SG-LB (reference the LB security group)
  • Outbound: DB port 5432 to SG-DB
  • Outbound: required services (cache, queue, observability) as needed

SG-DB

  • Inbound: 5432 from SG-App only
  • Outbound: usually allowed to respond (stateful makes this easy)

What this gives you:
Even if someone knows the app server IP, they cannot reach it directly. The only door is the load balancer.


Pattern 2: “Private database, no surprises”

Databases should almost never be reachable from public networks.

Subnet choice

  • Put DB in a private subnet (no direct internet route)

SG-DB

  • Only allow inbound from app SG on DB port
  • No inbound from anywhere else

Optional NACL guardrail

  • Deny inbound from internet ranges (or from public subnets) to DB subnets

What this prevents:
Accidental “open DB to world” incidents.


Pattern 3: “Admin access without opening SSH to the world”

Bad:

  • Inbound SSH 22 from 0.0.0.0/0 (never do this)

Better:

  • Bastion host in a controlled subnet
  • Allow SSH to bastion only from corporate IP/VPN
  • Allow SSH from bastion to private instances

SG-Bastion

  • Inbound: 22 from corporate IP range only
  • Outbound: 22 to SG-App (or to specific admin hosts)

SG-App

  • Inbound: 22 from SG-Bastion only

Even better (modern):

  • No inbound SSH at all; use session-based access through managed tools
    But even if you do bastion, keep it locked tight.

Pattern 4: “NACL as safety rails, not the steering wheel”

Here’s the best way to use NACLs:

Use NACLs for coarse controls like:

  • Block inbound SSH/RDP to public subnets (except bastion)
  • Block known-bad IP ranges
  • Ensure private subnets do not accept inbound from the internet directly
  • Prevent accidental exposure if someone misconfigures an SG

Don’t use NACLs for:

  • Complex app-to-app policies
  • Per-service microsegmentation
  • Fine-grained changes every sprint

Why?
Because stateless + rule ordering + ephemeral ports = high chance of breaking legitimate traffic.


Real-world patterns you’ll actually need (beyond 3-tier)

Pattern 5: “Default deny everywhere, allow by exception”

This is the mature posture:

  • No inbound unless specifically required
  • Minimal outbound to required dependencies only
  • Everything else blocked by default

In practice, many teams allow all outbound at SG level early on, then gradually tighten. The key is to have a plan to reduce “any/any egress” over time.


Pattern 6: “Egress control” (where most cost + data leaks happen)

Inbound attacks get attention. Egress is where real pain hides:

  • data exfiltration
  • malware calling home
  • surprise bills from uncontrolled downloads
  • dev instances pulling huge images all day

Three levels of egress maturity

Level 1: Allow all outbound (common, fast, risky)
Level 2: Allow outbound only to known internal services + required internet (still broad)
Level 3: Force all outbound through a firewall/proxy with allow-lists and logging (best control)

Practical example
App tier outbound allowed only to:

  • DB (5432)
  • Cache (6379)
  • Queue (443 to internal endpoint)
  • Observability endpoint (443)
  • Package repos (if needed) via controlled path

Pattern 7: “Hub-and-spoke with centralized firewall inspection”

This is standard for large orgs:

  • Spoke VPC/VNet per environment/team
  • All traffic to internet or between spokes goes through hub firewall
  • You get:
    • consistent policy
    • single place for logging/inspection
    • easier governance

Use SGs inside each spoke for microsegmentation.


Pattern 8: “Microsegmentation for east-west traffic”

East-west = service-to-service inside your network.

A strong approach:

  • SG per service (or per app component)
  • Allow only the required ports between them
  • Keep “shared” SGs minimal

Example:

  • SG-orders-api inbound only from SG-gateway on 443
  • SG-orders-api outbound only to SG-orders-db on 5432
  • SG-orders-worker inbound only from queue service, etc.

This reduces blast radius: one compromised service can’t laterally move everywhere.


Pattern 9: “Kubernetes reality” (EKS/AKS/GKE style)

Kubernetes adds another layer:

  • Node security groups / NSGs / firewall rules
  • Kubernetes network policies (if enabled)
  • Ingress controller rules

Practical guidance:

  • Use cloud SG/NSG for north-south boundaries (LB → nodes/pods)
  • Use NetworkPolicies for east-west pod-to-pod segmentation (when you’re ready)
  • Keep a clear map of which layer enforces what, or debugging becomes painful

The debugging playbook: find what’s blocking in 5 minutes

When a connection fails, stop guessing. Walk the path.

Step 1: Confirm the basics

  • Is the target IP/hostname correct?
  • Is the service listening on that port?
  • Is there a route between the networks?
  • Are you using the right protocol (TCP vs UDP)?

Step 2: Identify the exact flow

Write it down like this:

  • Source: (IP/subnet/workload)
  • Destination: (IP/subnet/workload)
  • Port: (e.g., 443)
  • Direction: inbound to destination

Step 3: Check layers in order

  1. Workload SG (destination side inbound rules)
  2. Workload SG (source side outbound rules)
  3. Subnet NACL (destination subnet inbound + source subnet outbound)
  4. Firewalls/NAT gateways in the path
  5. App-level: TLS, auth, WAF, etc.

Step 4: The classic pitfalls

  • NACL missing ephemeral port return range
  • SG inbound open, but SG outbound blocked
  • Rules applied to wrong subnet or wrong NIC
  • Firewall denies egress to a dependency (common with new SaaS endpoints)
  • “Allow from CIDR” used but source IP is different due to NAT/load balancer

Practical rule patterns you can copy

Pattern A: Public HTTPS load balancer + private app

  • LB: Inbound 443 from internet
  • App: Inbound only from LB security group on app port
  • No direct inbound to app from internet
  • DB inbound only from app

Pattern B: Strict admin access

  • No inbound admin ports in private tiers
  • Admin via bastion/VPN only
  • Separate SG for admin rules (easy to audit)

Pattern C: “Shared services” subnet

Put shared services (logging, monitoring, CI runners) in their own segment:

  • allow only necessary ports from app subnets
  • restrict egress from shared services too (they often have powerful credentials)

Pattern D: Environment isolation

  • Prod cannot talk to dev by default
  • Separate networks + firewall policy between them
  • If a specific integration is needed, allow only that integration explicitly

When should you use what?

Use Security Groups when:

  • You want precise, readable “who can talk to whom”
  • You need app-specific rules
  • You want safer day-to-day changes

Use NACLs when:

  • You want coarse subnet guardrails
  • You want explicit denies for known-bad sources
  • You want a safety net against accidental exposure

Use Firewalls when:

  • You need centralized egress control
  • You need inspection/logging for compliance
  • You have many networks and want consistent governance
  • You want segmentation across accounts/projects/environments

The “golden baseline” (starter configuration that works)

If you’re starting from scratch, this baseline is strong and practical:

  1. Public subnets
  • Only load balancers and bastion (if used)
  • NACL blocks inbound admin ports broadly (except bastion rules)
  1. Private app subnets
  • No direct internet inbound
  • SG inbound only from load balancer or internal gateway
  • Egress limited to required dependencies
  1. Private data subnets
  • Only DBs/caches
  • SG inbound only from app SG on specific ports
  • No outbound except what’s required (updates/backup endpoints via controlled routes)
  1. Central firewall or controlled egress (as you mature)
  • Route outbound through inspection
  • Log and alert on unexpected destinations

Final takeaway (the line you’ll reuse forever)

If you’re ever unsure which control to use, ask:

  • Is this about protecting a specific workload? → Security Group
  • Is this about guarding a subnet boundary? → NACL
  • Is this about governing traffic across networks or the internet? → Firewall

Then apply one of the patterns above instead of reinventing rules every time.


Category: 
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments