Network Segmentation: VLANs and Firewall Rules for Production
Network Segmentation: Isolating Services with VLANs and Firewall Rules
Section titled “Network Segmentation: Isolating Services with VLANs and Firewall Rules”Introduction: Why Your Flat Network is a Liability
Section titled “Introduction: Why Your Flat Network is a Liability”I once walked into a post-mortem where a single compromised WordPress plugin led to an attacker pivoting from a public web server to a PCI-compliant payment database in under 90 seconds. How? The network was flat — one VLAN, one broadcast domain, no firewall rules between services. The attacker ran arp-scan, found the DB server, and started exfiltrating credit card numbers over port 3306. The CISO asked me, “How do we stop this?” My answer: VLANs and firewall rules, applied correctly.
Flat networks are fine for your home lab. In production, they’re a breach waiting to happen. The problem is blast radius — if an attacker compromises one service, they can pivot laterally to everything. VLANs create logical barriers at Layer 2, and firewall rules enforce who talks to whom at Layers 3-4. Together, they’re the minimum viable defense for any multi-service environment.
This guide is for senior engineers who already know what a VLAN is but need to design, implement, and maintain segmentation that actually works. After reading, you’ll be able to kill lateral movement in your network, avoid the common misconfigurations that make segmentation useless, and sleep better at night.
Prerequisites
Section titled “Prerequisites”- Access to a managed switch (Cisco IOS, Aruba, or similar) with VLAN support
- A Linux host (Ubuntu 22.04+ or Debian 12) with
iptablesornftablesinstalled - Basic understanding of IP addressing and subnetting
- Root or sudo access on the Linux router/firewall
VLAN Fundamentals: The Logical Barrier You’re Probably Misconfiguring
Section titled “VLAN Fundamentals: The Logical Barrier You’re Probably Misconfiguring”VLAN Tagging 101 (802.1Q) — What Most Tutorials Get Wrong
Section titled “VLAN Tagging 101 (802.1Q) — What Most Tutorials Get Wrong”VLAN tagging using 802.1Q inserts a 4-byte tag into Ethernet frames to identify which VLAN a packet belongs to. Most tutorials skip the critical part: disable DTP (Dynamic Trunking Protocol) on every switch port that shouldn’t be a trunk. DTP negotiates trunking automatically, and if an attacker plugs into an access port that has DTP enabled, they can negotiate a trunk and perform VLAN hopping — accessing traffic from other VLANs.
Here’s the Cisco IOS config to kill DTP on access ports:
interface GigabitEthernet0/1 description Web Server Access Port switchport mode access switchport access vlan 10 no switchport trunk dynamic spanning-tree portfastThe switchport mode access forces the port to access mode and disables DTP negotiation. The spanning-tree portfast skips STP listening/learning for faster convergence on end-host ports. On Aruba/ProVision switches, the equivalent is:
interface 1/1 vlan access 10 no vlan trunk dynamicIn Linux bridging for virtualized environments (e.g., KVM with Linux bridge), you configure VLAN tagging at the interface level:
ip link add link eth0 name eth0.10 type vlan id 10ip addr add 10.0.10.1/24 dev eth0.10ip link set eth0.10 upThis creates a VLAN-tagged subinterface. The vlan.raw_device in Linux bridging (used by libvirt) lets you map a bridge to a specific VLAN — don’t confuse this with software-defined networking.
VLAN Design Patterns: The “One VLAN Per Service” Anti-Pattern
Section titled “VLAN Design Patterns: The “One VLAN Per Service” Anti-Pattern”I’ve seen teams create 50+ VLANs for a microservice architecture, one per container group. This is management hell. Every VLAN needs a subnet, a gateway, firewall rules, and DHCP scope. You’ll spend more time maintaining VLANs than actually securing services.
Advocate for “trust zones” — 5-10 VLANs max. Group services by their security posture and data sensitivity:
- DMZ (VLAN 10-19): Public-facing web servers, reverse proxies
- Internal (VLAN 20-29): Application servers, API gateways
- Restricted (VLAN 30-39): Databases, message queues
- Management (VLAN 100-109): SSH, monitoring, backup
Here’s a sane VLAN numbering scheme:
VLAN 10 - DMZ Web (10.0.10.0/24)VLAN 20 - Internal App (10.0.20.0/24)VLAN 30 - Restricted DB (10.0.30.0/24)VLAN 100 - Management (10.0.100.0/24)VLAN 999 - Native/Unused (10.0.999.0/24)On Linux, create these VLAN interfaces:
for vlan in 10 20 30 100 999; do ip link add link eth0 name eth0.$vlan type vlan id $vlan ip addr add 10.0.$vlan.1/24 dev eth0.$vlan ip link set eth0.$vlan updoneThis keeps Layer 2 boundaries clean without overcomplicating.
Native VLAN Gotchas — The Silent Security Hole
Section titled “Native VLAN Gotchas — The Silent Security Hole”The native VLAN (VLAN 1 by default on most switches) is the VLAN that untagged traffic travels on over a trunk port. If two switches have mismatched native VLANs, traffic from one VLAN can leak into another. I once debugged a “segmented” network where a web server in VLAN 10 could talk directly to a database in VLAN 30 because the trunk between switches had native VLAN 10 on one side and native VLAN 30 on the other.
Change the native VLAN to an unused VLAN (like 999) on all trunk ports:
interface GigabitEthernet0/24 description Trunk to Core Switch switchport trunk encapsulation dot1q switchport mode trunk switchport trunk native vlan 999 switchport trunk allowed vlan 10,20,30,100 no switchport trunk dynamicIn Linux bridging with bridge-vlan-aware mode (used by systemd-networkd or libvirt), ensure the bridge interface has bridge-vlan-unaware set for untagged ports. Misconfiguring this can cause VLAN leaks similar to native VLAN mismatches.
| Pitfall | Symptom | Fix |
|---|---|---|
| Mismatched native VLANs on trunk | Traffic leaks between VLANs | Set native VLAN to unused ID on both ends |
| DTP enabled on access port | VLAN hopping possible | switchport mode access + no switchport trunk dynamic |
| Spanning tree on access ports | Slow convergence after link flap | spanning-tree portfast on access ports |
Q: Should I use 802.1Q or QinQ? Use 802.1Q for internal segmentation. QinQ (double tagging) is for service providers or data center interconnects where you need to preserve customer VLAN tags. For most organizations, 802.1Q is sufficient and simpler to troubleshoot.
Firewall Rules: Where ACLs Go to Die (or Thrive)
Section titled “Firewall Rules: Where ACLs Go to Die (or Thrive)”Stateful vs. Stateless — Don’t Be That Person Using ACLs for Stateful Traffic
Section titled “Stateful vs. Stateless — Don’t Be That Person Using ACLs for Stateful Traffic”Stateless ACLs (like Cisco IOS extended ACLs) inspect each packet individually — they don’t track connection state. This means you must write rules for both directions (inbound and return traffic). Stateful firewalls (iptables, nftables, pfSense) track connections: if you allow an outbound SYN packet, the return SYN-ACK is automatically permitted.
Always use stateful firewalls for inter-VLAN routing. Here’s the iptables rule to allow established connections:
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPTThis single rule replaces dozens of stateless ACL entries. In nftables, the equivalent is:
nft add rule inet filter forward ct state established,related acceptFor pfSense (or other BSD-based firewalls), a typical inter-VLAN rule looks like:
pass in on vlan10 proto tcp from 10.0.10.0/24 to 10.0.20.0/24 port 5432 keep stateThe keep state keyword enables stateful inspection. Without it, you’re back to stateless ACLs.
The “Allow by Default” Fallacy — Why Explicit Deny Is Your Only Friend
Section titled “The “Allow by Default” Fallacy — Why Explicit Deny Is Your Only Friend”A single “allow all” rule between VLANs nullifies all segmentation. I once found a production firewall where a junior admin added iptables -A FORWARD -j ACCEPT “for debugging” and forgot to remove it. Six months later, we discovered the database cluster had been communicating with a staging environment over the internet because the firewall was wide open.
Set the default policy to DROP for all forwarding chains:
iptables -P FORWARD DROPIn nftables, set the policy at chain creation:
nft add chain inet filter forward { policy drop\; }On a Cisco ASA, the equivalent is:
access-list INTERNAL extended deny ip any anyThen write explicit allow rules for legitimate traffic. Every rule you add should answer the question: “Why does this service need to talk to that service?”
Micro-Segmentation — When VLANs Aren’t Enough (But They’re a Start)
Section titled “Micro-Segmentation — When VLANs Aren’t Enough (But They’re a Start)”VLANs isolate at Layer 2, firewalls at Layers 3-4, but modern threats exploit Layer 7. Micro-segmentation means restricting traffic to specific IP:port pairs, not just VLAN-to-VLAN. For example, instead of allowing all traffic from VLAN 10 (Web) to VLAN 20 (App), restrict it to only the specific app server and port.
Here’s how to lock down a web server to talk only to a specific DB server on port 5432:
iptables -A FORWARD -s 10.0.10.0/24 -d 10.0.20.5 -p tcp --dport 5432 -j ACCEPTiptables -A FORWARD -s 10.0.20.5 -d 10.0.10.0/24 -p tcp --sport 5432 -m state --state ESTABLISHED,RELATED -j ACCEPTThe second rule allows only return traffic from the DB server, not arbitrary outbound connections from the DB.
| Approach | Cost | Complexity | Security Level |
|---|---|---|---|
| VLAN-only (no firewall) | Low | Low | Poor — no traffic control |
| VLAN + per-VLAN firewall | Medium | Medium | Good — restricts at VLAN level |
| Micro-segmentation | High | High | Excellent — restricts per IP:port |
Q: Do I need a separate firewall for each VLAN? No. A single stateful firewall (or firewall context) can route between VLANs and enforce rules. Use a firewall-on-a-stick (one interface with multiple VLAN subinterfaces) or a Layer 3 switch with ACLs. Separate firewalls per VLAN are overkill unless you have compliance requirements for physical separation.
Routing Between VLANs — The Layer 3 Nightmare
Section titled “Routing Between VLANs — The Layer 3 Nightmare”Router-on-a-Stick vs. Layer 3 Switches — Pick Your Poison
Section titled “Router-on-a-Stick vs. Layer 3 Switches — Pick Your Poison”Router-on-a-stick uses a single physical interface with multiple VLAN subinterfaces to route between VLANs. It’s simple but creates a bottleneck — all inter-VLAN traffic must traverse that one link. For small networks (< 10 VLANs, < 1 Gbps inter-VLAN traffic), it’s fine. For anything larger, use a Layer 3 switch.
Here’s a Linux router-on-a-stick config:
# Create VLAN subinterfacesip link add link eth0 name eth0.10 type vlan id 10ip addr add 10.0.10.1/24 dev eth0.10ip link set eth0.10 up
ip link add link eth0 name eth0.20 type vlan id 20ip addr add 10.0.20.1/24 dev eth0.20ip link set eth0.20 upOn a Cisco Layer 3 switch, you create SVIs (Switch Virtual Interfaces):
interface Vlan10 description DMZ Web ip address 10.0.10.1 255.255.255.0 no shutdown
interface Vlan20 description Internal App ip address 10.0.20.1 255.255.255.0 no shutdownEnable IP routing globally:
ip routing| Approach | Cost | Throughput | Complexity |
|---|---|---|---|
| Router-on-a-stick | Low | Limited by single link | Low |
| Layer 3 switch | Medium | Wire-speed (hardware) | Medium |
| Firewall-on-a-stick | High | Depends on firewall | High |
The “Default Route to Nowhere” — How to Prevent Accidental Egress
Section titled “The “Default Route to Nowhere” — How to Prevent Accidental Egress”A default route sends all traffic with no specific match to a gateway. If you have a default route pointing to your internet gateway, a missing static route for an internal VLAN can cause traffic to leak to the internet. I once saw a database replication stream that was supposed to stay on a private VLAN accidentally route over the public internet because someone forgot to add a static route for the remote VLAN.
Use explicit static routes for inter-VLAN traffic, not a default gateway:
# Linux: Add static route for VLAN 20 via VLAN 10 gatewayip route add 10.0.20.0/24 via 10.0.10.1On Cisco:
ip route 10.0.20.0 255.255.255.0 10.0.10.1If you must use a default route, make it point to a null interface or a firewall that drops all traffic:
ip route add default via 10.0.0.1# Add a more specific deny for RFC1918 spaceip route add 10.0.0.0/8 via 10.0.0.1Q: Can I use BGP for inter-VLAN routing? Technically yes, but don’t. BGP is for routing between autonomous systems (e.g., different organizations or data centers). For internal VLAN routing, use static routes or OSPF. BGP adds unnecessary complexity and convergence time for a simple Layer 3 boundary.
Real-World Deployment: A 3-Tier Web Application
Section titled “Real-World Deployment: A 3-Tier Web Application”The Setup — Web, App, DB, and Management VLANs
Section titled “The Setup — Web, App, DB, and Management VLANs”Let’s walk through a concrete example. We have a 3-tier web application with four VLANs:
VLAN 10: Web (10.0.10.0/24) - Nginx reverse proxyVLAN 20: App (10.0.20.0/24) - Node.js API serversVLAN 30: DB (10.0.30.0/24) - PostgreSQL clusterVLAN 100: Management (10.0.100.0/24) - SSH, monitoring
Topology:[Internet] --- [Firewall] --- [Switch with VLANs] |-- VLAN 10: Web (10.0.10.10) |-- VLAN 20: App (10.0.20.20) |-- VLAN 30: DB (10.0.30.30) |-- VLAN 100: Mgmt (10.0.100.100)Full switch config for trunk ports (Cisco IOS):
interface GigabitEthernet0/24 description Trunk to Firewall switchport trunk encapsulation dot1q switchport mode trunk switchport trunk native vlan 999 switchport trunk allowed vlan 10,20,30,100 no switchport trunk dynamic spanning-tree portfast trunk
interface GigabitEthernet0/1 description Web Server switchport mode access switchport access vlan 10 no switchport trunk dynamic spanning-tree portfast
interface GigabitEthernet0/2 description App Server switchport mode access switchport access vlan 20 no switchport trunk dynamic spanning-tree portfast
interface GigabitEthernet0/3 description DB Server switchport mode access switchport access vlan 30 no switchport trunk dynamic spanning-tree portfast
interface GigabitEthernet0/4 description Management Host switchport mode access switchport access vlan 100 no switchport trunk dynamic spanning-tree portfastThe Rules — Minimum Viable Firewall Policy
Section titled “The Rules — Minimum Viable Firewall Policy”Here’s the full nftables ruleset for inter-VLAN traffic. This enforces:
- Web-to-App: allow HTTP/HTTPS (ports 80, 443)
- App-to-DB: allow PostgreSQL (port 5432)
- Management-to-all: allow SSH (port 22)
- Deny all else
Create /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter { chain forward { type filter hook forward priority 0; policy drop;
# Allow established connections ct state established,related accept
# Management VLAN (100) can SSH to all iifname "eth0.100" tcp dport 22 accept
# Web VLAN (10) to App VLAN (20): HTTP/HTTPS iifname "eth0.10" oifname "eth0.20" tcp dport { 80, 443 } accept
# App VLAN (20) to DB VLAN (30): PostgreSQL iifname "eth0.20" oifname "eth0.30" tcp dport 5432 accept
# Log dropped packets (rate-limited) log prefix "DROPPED: " group 0 }
chain input { type filter hook input priority 0; policy drop;
# Allow established connections ct state established,related accept
# Allow SSH from management VLAN iifname "eth0.100" tcp dport 22 accept
# Allow ICMP (for troubleshooting) ip protocol icmp accept
# Log dropped packets log prefix "INPUT DROP: " group 0 }
chain output { type filter hook output priority 0; policy accept; }}Apply the ruleset:
nft -f /etc/nftables.confFor iptables users, here’s the equivalent iptables-save output:
# Generated by iptables-save*filter:INPUT DROP [0:0]:FORWARD DROP [0:0]:OUTPUT ACCEPT [0:0]-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT-A INPUT -i eth0.100 -p tcp --dport 22 -j ACCEPT-A INPUT -p icmp -j ACCEPT-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT-A FORWARD -i eth0.100 -p tcp --dport 22 -j ACCEPT-A FORWARD -i eth0.10 -o eth0.20 -p tcp -m multiport --dports 80,443 -j ACCEPT-A FORWARD -i eth0.20 -o eth0.30 -p tcp --dport 5432 -j ACCEPT-A FORWARD -j LOG --log-prefix "FORWARD DROP: " --log-level 4-A INPUT -j LOG --log-prefix "INPUT DROP: " --log-level 4COMMITTesting Your Segmentation — Don’t Assume It Works
Section titled “Testing Your Segmentation — Don’t Assume It Works”I’ve seen too many “segmented” networks where someone forgot the deny rule. Test thoroughly. From a web server in VLAN 10, run:
# Test allowed traffic: should worknmap -sS -p 80,443 10.0.20.20
# Test denied traffic: should failnmap -sS -p 22 10.0.20.20nmap -sS -p 5432 10.0.30.30
# Capture traffic to verifytcpdump -i eth0.10 host 10.0.20.20 and port 5432Use hping3 to test specific protocols:
# Test TCP SYN to denied porthping3 -S -p 5432 -c 3 10.0.20.20
# Test UDP (if applicable)hping3 -2 -p 161 -c 3 10.0.30.30Don’t rely on ping alone. ICMP might be blocked but TCP works, or vice versa. Always test the actual service ports.
Q: How do I handle VLANs in cloud environments (AWS, Azure)? Cloud providers don’t use traditional VLANs. Use security groups (L4 stateful firewalls) and network ACLs (stateless) for the same effect. For multi-VPC segmentation, use VPC peering with explicit route tables and security group rules. The principles are identical — default-deny, least privilege, and micro-segmentation.
Monitoring and Maintenance — Because Segmentation Drifts
Section titled “Monitoring and Maintenance — Because Segmentation Drifts”Logging — Your Firewall Rules Are Useless Without It
Section titled “Logging — Your Firewall Rules Are Useless Without It”If you don’t log dropped packets, you won’t know when segmentation fails. But don’t flood syslog — use rate-limited logging. In nftables, the log statement with a prefix sends to syslog:
nft add rule inet filter forward log prefix "DROPPED: " group 0The group 0 uses the kernel’s default logging rate limit (10 messages per second). For iptables:
iptables -A FORWARD -j LOG --log-prefix "DROPPED: " --log-level 4Forward these logs to a SIEM (Wazuh, Splunk, or ELK). Configure rsyslog to send to a remote server:
:msg, contains, "DROPPED: " @@192.168.1.100:514Now you can alert on patterns like “multiple drops from same source” — that’s a scanning attempt or a misconfigured service.
Auditing — The “Who Changed That Rule?” Problem
Section titled “Auditing — The “Who Changed That Rule?” Problem”Manual iptables-save edits are a crime. Version-control your firewall rules with git. Here’s an Ansible playbook to apply nftables rules from a git repo:
---- name: Apply nftables rules hosts: firewalls tasks: - name: Copy nftables config from git ansible.builtin.git: repo: 'git@github.com:your-org/firewall-rules.git' dest: /etc/nftables/ version: main notify: reload nftables
- name: Validate nftables syntax ansible.builtin.command: cmd: nft -c -f /etc/nftables/nftables.conf changed_when: false
handlers: - name: reload nftables ansible.builtin.systemd: name: nftables state: restartedAdd a git pre-commit hook to validate syntax before anyone pushes:
#!/bin/bashnft -c -f /etc/nftables.confif [ $? -ne 0 ]; then echo "ERROR: nftables syntax check failed. Fix before committing." exit 1fiWar story: I found a “temporary” iptables rule in a production firewall that had been there for 3 years. The comment said ”# TODO: remove after testing”. The rule allowed all traffic from the DMZ to the internal network. The original admin had left the company 2 years prior. Version control would have caught this.
Q: How often should I review my firewall rules?
Quarterly, minimum. More frequently if you have compliance requirements (PCI DSS requires semi-annual reviews). Use a tool like nft list ruleset or iptables-save to diff against the last known-good version. If you see rules without comments or with # TODO in them, flag them for immediate review.
Common Mistakes and How to Avoid Them
Section titled “Common Mistakes and How to Avoid Them”-
Using VLAN 1 as the native VLAN: Default VLAN 1 is universally known and often unchangeable on some switches. Always set the native VLAN to an unused ID (e.g., 999) on all trunk ports.
-
Forgetting to log drops: Without logs, you can’t troubleshoot or detect attacks. At minimum, log dropped packets with rate limiting.
-
Relying on ping for segmentation tests: ICMP might be allowed while TCP is blocked, or vice versa. Always test the actual service ports with
nmaporhping3. -
Allowing all traffic “temporarily”: A temporary allow-all rule becomes permanent. Use explicit allow rules with comments explaining why they exist and when they can be removed.
-
Not version-controlling firewall rules: Manual edits drift over time. Use git + Ansible or similar to enforce a single source of truth.
Q: What’s the ROI of network segmentation? The direct ROI is reduced blast radius. A single compromised service can’t pivot to critical systems. Indirectly, it simplifies compliance (PCI DSS, HIPAA, SOC 2 all require network segmentation) and reduces incident response time. The cost of implementing VLANs and firewalls is orders of magnitude less than a data breach.
Q: Can I use VLANs with containers? Yes, but it’s more complex. In Docker, use macvlan or ipvlan networks with VLAN tagging. For Kubernetes, use network policies (which operate at Layer 3-4) combined with VLAN-backed CNI plugins like Calico. The same principles apply: default-deny, least privilege, and micro-segmentation.
Q: How do I handle guest Wi-Fi with VLANs? Put guest Wi-Fi on a separate VLAN (e.g., VLAN 200) with a firewall rule that allows only internet access (no traffic to internal VLANs). Use a captive portal or WPA2-Enterprise with 802.1X for authentication. Never let guest traffic touch your production network.
Q: What about IPv6? IPv6 doesn’t use ARP, but VLANs work the same way. Apply the same firewall rules to IPv6 traffic. Be aware that SLAAC (Stateless Address Autoconfiguration) can make IP addresses unpredictable —