Network Security: Segmentation and Firewalls

Application-layer security gets most of the attention these days. We obsess over input validation, authentication tokens, and API security—and rightfully so. But network-level controls remain...

Key Insights

  • Network segmentation remains your most effective defense against lateral movement—attackers who breach one system shouldn’t automatically access everything else
  • Zero trust architecture isn’t a product you buy; it’s a design principle requiring identity verification at every network hop, not just the perimeter
  • Cloud environments demand a different mental model: security groups and network policies replace traditional firewalls, but the core principles of least-privilege access still apply

Introduction to Network Security Fundamentals

Application-layer security gets most of the attention these days. We obsess over input validation, authentication tokens, and API security—and rightfully so. But network-level controls remain foundational to any serious security posture.

Defense in depth isn’t just a buzzword. When your web application firewall fails to catch a novel attack, network segmentation limits blast radius. When an attacker compromises a developer workstation, properly configured firewalls prevent them from pivoting to production databases.

The reality is that most breaches involve lateral movement. Attackers rarely hit their target on the first hop. They compromise something accessible, then move laterally until they find valuable data. Network security controls exist to make that lateral movement as difficult as possible.

Network Segmentation Principles

Segmentation divides your network into isolated zones with controlled traffic flow between them. The goal is simple: systems that don’t need to communicate shouldn’t be able to communicate.

VLANs and Subnets provide the foundational layer. VLANs operate at Layer 2, creating logical broadcast domains. Subnets operate at Layer 3, defining IP address ranges that route through gateways. Together, they create network boundaries.

Trust zones group systems by sensitivity and function. A typical architecture includes:

  • Public-facing zone (web servers, load balancers)
  • Application zone (business logic, APIs)
  • Data zone (databases, file storage)
  • Management zone (monitoring, deployment tools)

Micro-segmentation takes this further, applying controls at the workload level rather than network segment level. In containerized environments, this means controlling traffic between individual pods or services.

Here’s a practical Terraform configuration implementing proper VPC segmentation in AWS:

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "production-vpc"
  }
}

# Public subnet for load balancers and bastion hosts
resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index}"
    Tier = "public"
  }
}

# Private subnet for application servers
resource "aws_subnet" "application" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "application-subnet-${count.index}"
    Tier = "private"
  }
}

# Isolated subnet for databases - no internet access
resource "aws_subnet" "database" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 20}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "database-subnet-${count.index}"
    Tier = "isolated"
  }
}

# NAT Gateway for private subnet outbound access
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id
}

# Route table ensuring database subnet has no internet route
resource "aws_route_table" "database" {
  vpc_id = aws_vpc.main.id

  # No route to internet gateway or NAT - fully isolated
  tags = {
    Name = "database-route-table"
  }
}

This configuration creates clear separation: public resources face the internet, application servers access the internet through NAT for updates, and databases remain completely isolated.

Firewall Types and Architecture

Understanding firewall capabilities helps you choose the right tool for each location in your architecture.

Packet filtering examines individual packets against rules based on source/destination IP, ports, and protocols. Fast but stateless—it can’t track connection state or understand application protocols.

Stateful inspection tracks connection state, allowing return traffic for established connections. This is the baseline for modern firewalls and what you get with iptables or cloud security groups.

Next-generation firewalls (NGFW) add application awareness, inspecting traffic content to identify applications regardless of port. They integrate IDS/IPS, URL filtering, and threat intelligence.

Placement matters. Perimeter firewalls filter traffic entering your network. Internal firewalls control traffic between segments. Host-based firewalls protect individual systems and are essential for defense in depth.

Here’s a practical iptables configuration for a Linux application server:

#!/bin/bash
# Host firewall for application server

# Flush existing rules
iptables -F
iptables -X

# Default policies: deny all incoming, allow outgoing
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# SSH from management subnet only
iptables -A INPUT -p tcp --dport 22 -s 10.0.100.0/24 -j ACCEPT

# Application port from load balancer subnet
iptables -A INPUT -p tcp --dport 8080 -s 10.0.0.0/24 -j ACCEPT

# Health checks from load balancer
iptables -A INPUT -p tcp --dport 8081 -s 10.0.0.0/24 -j ACCEPT

# Prometheus metrics from monitoring subnet
iptables -A INPUT -p tcp --dport 9090 -s 10.0.100.0/24 -j ACCEPT

# Log and drop everything else
iptables -A INPUT -j LOG --log-prefix "DROPPED: "
iptables -A INPUT -j DROP

# Save rules
iptables-save > /etc/iptables/rules.v4

This configuration implements least privilege: only specific ports from specific subnets are allowed. Everything else is logged and dropped.

Implementing Zero Trust Network Architecture

Zero trust abandons the concept of a trusted internal network. Every request must be authenticated and authorized, regardless of network location.

The core principles:

  • Verify explicitly: Authenticate and authorize based on all available data points
  • Least privilege access: Just-in-time and just-enough access
  • Assume breach: Minimize blast radius and segment access

In Kubernetes environments, NetworkPolicies implement zero trust at the pod level:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow traffic from ingress controller only
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
          podSelector:
            matchLabels:
              app: nginx-ingress
      ports:
        - protocol: TCP
          port: 8080
    # Allow Prometheus scraping from monitoring namespace
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090
  egress:
    # Allow DNS
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
    # Allow database access
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    # Allow external API calls through egress gateway
    - to:
        - namespaceSelector:
            matchLabels:
              name: egress
          podSelector:
            matchLabels:
              app: egress-gateway
      ports:
        - protocol: TCP
          port: 443

This policy ensures the API server can only receive traffic from the ingress controller and Prometheus, and can only connect to the database, DNS, and an egress gateway for external calls.

Cloud-Native Network Security

Cloud providers offer network security primitives that differ from traditional firewalls but serve similar purposes.

Security Groups act as stateful firewalls attached to resources. They’re your primary control mechanism.

Network ACLs provide stateless subnet-level filtering. Use them as a backup layer, not your primary control.

Cloud Firewalls (AWS Network Firewall, Azure Firewall, GCP Cloud Firewall) add deep packet inspection and centralized management.

Here’s a CloudFormation template for properly configured security groups:

AWSTemplateFormatVersion: '2010-09-09'
Description: Security groups implementing defense in depth

Resources:
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Public-facing load balancer
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS from internet
      Tags:
        - Key: Name
          Value: alb-sg

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Application servers
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
          Description: Traffic from load balancer only
      Tags:
        - Key: Name
          Value: app-sg

  DatabaseSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Database servers
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !Ref ApplicationSecurityGroup
          Description: PostgreSQL from application tier only
      Tags:
        - Key: Name
          Value: db-sg

Notice the pattern: each tier only accepts traffic from the tier above it. The database never sees traffic from the load balancer directly.

Monitoring and Incident Response

Security controls without visibility are incomplete. You need to know when rules trigger and when unusual patterns emerge.

Enable VPC Flow Logs in AWS, NSG Flow Logs in Azure, or VPC Flow Logs in GCP. These capture metadata about every network connection.

Here’s a Python script for parsing firewall logs and detecting potential lateral movement:

#!/usr/bin/env python3
import json
from collections import defaultdict
from datetime import datetime, timedelta

def parse_flow_logs(log_file):
    """Parse VPC flow logs and detect anomalies."""
    connections = defaultdict(lambda: defaultdict(set))
    rejected_counts = defaultdict(int)
    
    with open(log_file) as f:
        for line in f:
            record = json.loads(line)
            
            src_ip = record['srcaddr']
            dst_ip = record['dstaddr']
            dst_port = record['dstport']
            action = record['action']
            
            if action == 'REJECT':
                rejected_counts[src_ip] += 1
            
            # Track unique destination IPs per source
            connections[src_ip]['destinations'].add(dst_ip)
            connections[src_ip]['ports'].add(dst_port)
    
    # Detect potential port scanning
    for src_ip, data in connections.items():
        if len(data['ports']) > 20:
            print(f"ALERT: Potential port scan from {src_ip}")
            print(f"  Unique ports accessed: {len(data['ports'])}")
    
    # Detect potential lateral movement
    for src_ip, data in connections.items():
        if len(data['destinations']) > 10:
            print(f"ALERT: Potential lateral movement from {src_ip}")
            print(f"  Unique destinations: {len(data['destinations'])}")
    
    # High rejection rates indicate probing
    for src_ip, count in rejected_counts.items():
        if count > 100:
            print(f"ALERT: High rejection count for {src_ip}: {count}")

if __name__ == '__main__':
    parse_flow_logs('/var/log/vpc-flow-logs.json')

Common Pitfalls and Best Practices

Over-permissive rules are the most common problem. That “temporary” rule allowing all traffic from the developer subnet has been there for three years. Audit regularly.

Rule sprawl happens when teams add rules without removing obsolete ones. Implement rule expiration and require justification for renewals.

Documentation gaps make incident response slower. Every rule should have a description explaining its purpose and owner.

Hardening checklist:

  • Default deny on all firewalls
  • No rules allowing 0.0.0.0/0 except public endpoints
  • Separate management traffic from application traffic
  • Log all denied connections
  • Review rules quarterly
  • Test rules after changes with actual traffic
  • Use infrastructure as code—no manual rule changes

Network security isn’t glamorous, but it’s essential. Implement these controls properly, and you’ll significantly reduce your attack surface and limit damage when breaches occur.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.