Chapter 22: Firewalls, IDS, IPS, and WAFs — Layers of Network Defense
"A castle with only one wall is a monument to optimism." — Bruce Schneier (paraphrased)
An HTTP request traveling from the internet to your application server passes through multiple inspection points: a firewall, an IPS, a load balancer, a WAF, and then the application itself. That is not overkill — each layer catches different things. The firewall blocks ports and protocols. The IPS catches known exploit patterns in network traffic. The WAF inspects HTTP specifically — request bodies, headers, cookies. The application validates business logic. Remove any one layer, and a specific class of attack sails through undetected.
This is defense in depth. This chapter shows you what each layer actually does under the hood, how to configure them, and — crucially — what they miss.
Packet Filtering Firewalls
The oldest and simplest form of firewall, dating back to the late 1980s. Packet filters examine individual packets against a rule set and make allow/deny decisions based on:
- Source and destination IP address
- Source and destination port
- Protocol (TCP, UDP, ICMP)
- Interface direction (inbound/outbound)
- TCP flags (SYN, ACK, FIN, RST)
They operate at Layer 3 (Network) and Layer 4 (Transport) of the OSI model. They see individual packets, not connections or application data.
flowchart TD
A[Incoming Packet] --> B{Extract headers:<br/>Src IP, Dst IP,<br/>Src Port, Dst Port,<br/>Protocol, Flags}
B --> C{Match Rule 1?<br/>ALLOW TCP dst 443}
C -->|Match| D[ALLOW — forward packet]
C -->|No match| E{Match Rule 2?<br/>ALLOW TCP dst 80}
E -->|Match| D
E -->|No match| F{Match Rule 3?<br/>DENY TCP dst 22<br/>from external}
F -->|Match| G[DENY — drop packet]
F -->|No match| H{Match Rule N?}
H -->|No match| I{Default Policy}
I -->|DROP| G
I -->|ACCEPT| D
style D fill:#2ecc71,stroke:#27ae60,color:#fff
style G fill:#ff6b6b,stroke:#c0392b,color:#fff
Fundamental limitation: Packet filters see individual packets, not connections. They cannot determine whether a packet is part of a legitimate established session or a spoofed response. An attacker can craft packets with the ACK flag set to bypass rules that only block SYN packets — the firewall sees an ACK and assumes it is part of an existing connection.
Stateful Inspection Firewalls
Stateful firewalls maintain a connection tracking table (also called a state table) that records active sessions. They understand the lifecycle of a TCP connection — the three-way handshake, the established state, and proper teardown.
stateDiagram-v2
[*] --> NEW: SYN packet arrives<br/>Check rules for NEW connections
NEW --> SYN_SENT: Rule allows → add to state table
NEW --> DROPPED: Rule denies → drop
SYN_SENT --> ESTABLISHED: SYN-ACK + ACK seen<br/>Three-way handshake complete
ESTABLISHED --> ESTABLISHED: Data packets<br/>Auto-allowed (in state table)
ESTABLISHED --> TIME_WAIT: FIN/RST seen<br/>Connection closing
TIME_WAIT --> [*]: Timeout → remove from table
state "Connection Tracking Table" as CTT {
[*] --> Entry1: Src: 203.0.113.50:49152<br/>Dst: 10.0.1.100:443<br/>State: ESTABLISHED<br/>Timeout: 3600s
[*] --> Entry2: Src: 198.51.100.7:51234<br/>Dst: 10.0.1.100:80<br/>State: SYN_RECV<br/>Timeout: 120s
[*] --> Entry3: Src: 10.0.2.50:38921<br/>Dst: 93.184.216.34:443<br/>State: ESTABLISHED<br/>Timeout: 3600s
}
The key advantage: return traffic for an established connection is automatically allowed without needing an explicit rule. This means you only need to write rules for new connections — simplifying rule sets and preventing spoofed response packets.
# Stateful firewall rules (conceptual)
# Rule 1: Allow return traffic for established connections
ALLOW state=ESTABLISHED,RELATED
# Rule 2: Allow new HTTPS connections from anywhere
ALLOW state=NEW proto=TCP dst_port=443
# Rule 3: Allow new SSH from management network only
ALLOW state=NEW proto=TCP src=10.0.0.0/24 dst_port=22
# Default: Drop everything else
DROP all
State table exhaustion: The state table has a limited size. A SYN flood attack sends millions of SYN packets without completing the handshake, filling the state table with half-open connections. When the table is full, no new connections can be tracked — even legitimate ones. Defense: SYN cookies, connection rate limiting, and large state tables.
iptables: The Linux Firewall — Chain Traversal
On Linux, the firewall is built into the kernel via Netfilter. Here is exactly how a packet traverses the system.
iptables Chain Traversal
flowchart TD
A[Packet arrives<br/>on network interface] --> B[PREROUTING chain<br/>nat table: DNAT]
B --> C{Destination<br/>is this host?}
C -->|Yes| D[INPUT chain<br/>filter table]
C -->|No| E[FORWARD chain<br/>filter table]
D --> F{Rules match?}
F -->|ACCEPT| G[Local Process]
F -->|DROP| H[Packet discarded]
E --> I{Rules match?}
I -->|ACCEPT| J[POSTROUTING chain<br/>nat table: SNAT/MASQ]
I -->|DROP| H
J --> K[Packet forwarded<br/>out another interface]
G --> L[Local process<br/>generates response]
L --> M[OUTPUT chain<br/>filter table]
M --> N{Rules match?}
N -->|ACCEPT| O[POSTROUTING chain<br/>nat table: SNAT]
N -->|DROP| H
O --> P[Packet sent out<br/>network interface]
style H fill:#ff6b6b,stroke:#c0392b,color:#fff
style G fill:#2ecc71,stroke:#27ae60,color:#fff
style K fill:#2ecc71,stroke:#27ae60,color:#fff
Complete iptables Server Configuration
#!/bin/bash
# firewall.sh — Production server firewall rules
# Apply with: sudo bash firewall.sh
# Flush existing rules
iptables -F
iptables -X
iptables -t nat -F
# Set default policies — DROP everything, then allowlist
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow loopback interface (critical for local services)
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established and related connections (stateful)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop invalid packets (malformed, out-of-state)
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Anti-spoofing: drop packets with source matching our own IP
iptables -A INPUT -s 10.0.1.100 ! -i lo -j DROP
# SSH from management network only, rate limited
iptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 \
-m conntrack --ctstate NEW \
-m recent --set --name SSH
iptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 \
-m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
iptables -A INPUT -s 10.0.0.0/24 -p tcp --dport 22 \
-m conntrack --ctstate NEW -j ACCEPT
# HTTP and HTTPS from anywhere
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT
# ICMP (ping) with rate limiting — 1 per second, burst of 4
iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 1/s --limit-burst 4 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
# SYN flood protection
iptables -A INPUT -p tcp --syn -m limit --limit 25/s --limit-burst 50 -j ACCEPT
iptables -A INPUT -p tcp --syn -j DROP
# Log dropped packets for debugging (rate limited to prevent log flooding)
iptables -A INPUT -m limit --limit 5/min --limit-burst 10 \
-j LOG --log-prefix "IPTABLES-DROP: " --log-level 4
# Drop everything else (explicit, matches default policy)
iptables -A INPUT -j DROP
echo "Firewall rules applied. $(iptables -L -n | wc -l) rules active."
Always allow loopback (`-i lo`) and established connections (`ESTABLISHED,RELATED`) before setting a default DROP policy. Without these, you will lock yourself out of the server and break all outbound connections. If working remotely via SSH, use a safety net:
~~~bash
# Safety net: flush rules in 5 minutes if you lose access
echo "iptables -F && iptables -P INPUT ACCEPT" | at now + 5 minutes
# Now apply your new rules — if you get locked out,
# the at job restores access in 5 minutes
sudo bash firewall.sh
# If everything works, cancel the safety net
atrm $(atq | tail -1 | awk '{print $1}')
~~~
nftables: The Modern Replacement
nftables replaces iptables with a cleaner syntax, better performance, and unified handling of IPv4, IPv6, and ARP:
#!/usr/sbin/nft -f
# /etc/nftables.conf
flush ruleset
table inet filter {
# Rate limiting set for SSH
set ssh_meter {
type ipv4_addr
flags dynamic
timeout 60s
}
chain input {
type filter hook input priority 0; policy drop;
# Loopback
iifname "lo" accept
# Connection tracking
ct state established,related accept
ct state invalid drop
# SSH from management with rate limiting
ip saddr 10.0.0.0/24 tcp dport 22 ct state new \
meter ssh_meter { ip saddr limit rate 3/minute burst 5 packets } accept
# Web traffic
tcp dport { 80, 443 } ct state new accept
# ICMP rate limited
icmp type echo-request limit rate 1/second burst 4 packets accept
# SYN flood protection
tcp flags syn limit rate 25/second burst 50 packets accept
# Log and count before dropping
limit rate 5/minute burst 10 packets \
log prefix "nft-drop: " counter drop
# Counter for all other drops
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# Apply nftables configuration
sudo nft -f /etc/nftables.conf
# List current ruleset
sudo nft list ruleset
# List with handles (for deletion)
sudo nft -a list chain inet filter input
# Add a rule dynamically
sudo nft add rule inet filter input tcp dport 8080 accept
# Delete a rule by handle number
sudo nft delete rule inet filter input handle 15
# Monitor rule hit counters
sudo nft list chain inet filter input | grep counter
Practice firewall rules on a test VM:
~~~bash
# Start with a permissive policy and logging
sudo iptables -P INPUT ACCEPT
sudo iptables -A INPUT -j LOG --log-prefix "FW-AUDIT: "
# Watch what traffic arrives
sudo tail -f /var/log/kern.log | grep "FW-AUDIT"
# From another machine, scan the target
nmap -sS -p 22,80,443,3306,5432,6379 target-ip
# Now apply restrictive rules and test again
sudo iptables -P INPUT DROP
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
sudo iptables -A INPUT -j LOG --log-prefix "FW-DROP: "
sudo iptables -A INPUT -j DROP
# Re-scan — only port 443 should show as open
nmap -sS -p 22,80,443,3306,5432,6379 target-ip
~~~
Intrusion Detection Systems (IDS)
Firewalls decide what traffic is allowed based on headers. An IDS examines traffic content and behavior to determine what is malicious. It monitors network traffic (or host activity) and generates alerts when it detects potential attacks. It does not block traffic — it is a passive monitoring system.
NIDS vs HIDS
graph LR
subgraph "Network IDS (NIDS)"
TAP[Network TAP<br/>or Mirror Port] --> SNORT[Snort / Suricata / Zeek]
SNORT --> ALERTS1[Alerts → SIEM]
NOTE1[Sees: All network traffic<br/>Cannot see: Encrypted content<br/>Deployment: Network tap/span port]
end
subgraph "Host IDS (HIDS)"
AGENT[Agent on Server] --> OSSEC[OSSEC / Wazuh / AIDE]
OSSEC --> ALERTS2[Alerts → SIEM]
NOTE2[Sees: File changes, processes,<br/>system calls, decrypted content<br/>Deployment: Agent per server]
end
Snort Rule Anatomy
Snort rules are the lingua franca of network intrusion detection. Understanding their structure is essential for both writing custom rules and tuning false positives:
alert tcp $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (
msg:"SQL Injection attempt - UNION SELECT";
flow:to_server,established;
content:"UNION"; nocase;
content:"SELECT"; nocase; distance:0; within:20;
pcre:"/UNION\s+(ALL\s+)?SELECT/i";
classtype:web-application-attack;
sid:1000001; rev:3;
reference:url,owasp.org/www-community/attacks/SQL_Injection;
metadata:severity high, confidence medium;
)
Rule breakdown:
| Component | Meaning |
|---|---|
alert | Action — generate alert (vs drop, reject, pass) |
tcp | Protocol |
$EXTERNAL_NET any | Source IP (any external) and port (any) |
-> | Direction (one-way) |
$HTTP_SERVERS $HTTP_PORTS | Destination — defined variables |
msg: | Alert message text |
flow:to_server,established | Only match on established TCP connections going to server |
content:"UNION" | Byte pattern to match in payload |
nocase | Case-insensitive matching |
distance:0; within:20 | "SELECT" must appear within 20 bytes after "UNION" |
pcre: | Perl-Compatible Regular Expression for complex matching |
classtype: | Attack category classification |
sid: | Unique rule ID (1000000+ for custom rules) |
rev: | Rule revision number |
Additional Snort rule examples:
# Detect Shellshock (CVE-2014-6271)
alert tcp $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (
msg:"SHELLSHOCK attempt in HTTP header";
flow:to_server,established;
content:"() {"; fast_pattern;
content:";";
sid:1000002; rev:1;
classtype:attempted-admin;
)
# Detect directory traversal
alert tcp $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (
msg:"Directory Traversal attempt";
flow:to_server,established;
content:".."; content:"/";
pcre:"/\.\.[\\\/]/";
sid:1000003; rev:1;
classtype:web-application-attack;
)
# Detect outbound connection to known C2 IP
alert tcp $HOME_NET any -> 198.51.100.0/24 any (
msg:"Outbound connection to known C2 network";
flow:to_server,established;
sid:1000004; rev:1;
classtype:trojan-activity;
)
Signature-Based vs Anomaly-Based Detection
| Aspect | Signature-Based | Anomaly-Based |
|---|---|---|
| How it works | Compares traffic to known attack patterns | Establishes baseline of "normal," alerts on deviations |
| Catches | Known attacks with high accuracy | Novel/zero-day attacks |
| Misses | Zero-day attacks, unknown variants | Attacks that mimic normal behavior |
| False positives | Low (well-tuned signatures) | Higher (normal variations trigger alerts) |
| Maintenance | Constant signature updates needed | Requires training period and ongoing tuning |
| Examples | Snort signatures, Suricata rules | ML-based UEBA, statistical models |
Intrusion Prevention Systems (IPS)
An IPS is an IDS that sits inline in the traffic path and can actively block traffic, not just alert.
graph LR
subgraph "IDS Deployment (Passive)"
T1[Traffic] -->|Original path| S1[Server]
T1 -->|Copy via TAP/mirror| IDS1[IDS]
IDS1 -->|Alerts only| SIEM1[SIEM]
end
subgraph "IPS Deployment (Inline)"
T2[Traffic] --> IPS1[IPS]
IPS1 -->|Clean traffic| S2[Server]
IPS1 -->|Malicious traffic| DROP1[DROP]
IPS1 -->|Alerts| SIEM2[SIEM]
end
style DROP1 fill:#ff6b6b,stroke:#c0392b,color:#fff
IPS deployment considerations:
| Concern | Impact | Mitigation |
|---|---|---|
| False positives | Block legitimate users | Start in IDS mode, tune rules, then enable blocking |
| Performance | Latency added to every packet | Hardware acceleration, bypass mode, multi-threaded engines |
| Fail mode | What happens when IPS crashes? | Fail-open (less secure) vs fail-closed (causes outage) |
| Encrypted traffic | Cannot inspect HTTPS content | TLS termination before IPS, or certificate-based inspection |
| Evasion | Fragmentation, encoding tricks | Protocol normalization, reassembly before inspection |
The IPS must be confident before blocking. Most deployments start in IDS mode (alert only) for weeks or months, tune the signatures to eliminate false positives, then switch specific high-confidence rules to IPS mode (block). You never go from zero to "block everything the IPS flags" on day one. A false positive in IDS mode is an alert someone reviews. A false positive in IPS mode is a customer who cannot use your service.
Suricata: Modern Network Security Monitoring
Suricata is the modern open-source alternative to Snort with significant advantages:
- Multi-threaded architecture — scales to 10+ Gbps on commodity hardware
- Native protocol detection — identifies HTTP, TLS, DNS, SMB, etc. on any port
- Built-in TLS/JA3/JA4 fingerprinting — identify clients by TLS behavior
- EVE JSON logging — structured, parseable output for SIEM integration
- Lua scripting — custom detection logic beyond signatures
- Automatic protocol parsing — extracts HTTP headers, DNS queries, TLS certificates
# Install and configure Suricata
sudo apt install suricata
sudo suricata-update enable-source et/open
sudo suricata-update
# Run in live capture mode
sudo suricata -c /etc/suricata/suricata.yaml -i eth0
# Run against a pcap file for analysis
sudo suricata -r captured_traffic.pcap -l /var/log/suricata/
# View fast alerts
tail -f /var/log/suricata/fast.log
Suricata EVE JSON Logging
The EVE JSON log provides rich structured data for every event — far more useful than flat text logs:
{
"timestamp": "2025-03-12T14:23:01.123456+0000",
"flow_id": 1234567890,
"event_type": "alert",
"src_ip": "203.0.113.50",
"src_port": 49152,
"dest_ip": "10.0.1.100",
"dest_port": 443,
"proto": "TCP",
"alert": {
"action": "allowed",
"gid": 1,
"signature_id": 2000001,
"rev": 5,
"signature": "ET WEB_SERVER SQL Injection Attempt",
"category": "Web Application Attack",
"severity": 1
},
"http": {
"hostname": "app.example.com",
"url": "/api/users?id=1' UNION SELECT",
"http_method": "GET",
"http_user_agent": "sqlmap/1.7",
"status": 200,
"length": 1523
},
"tls": {
"subject": "CN=app.example.com",
"issuerdn": "CN=Let's Encrypt Authority X3",
"ja3": {
"hash": "e7d705a3286e19ea42f587b344ee6865"
}
}
}
# Parse EVE JSON for specific alert types
cat /var/log/suricata/eve.json | python3 -c "
import sys, json
for line in sys.stdin:
evt = json.loads(line)
if evt.get('event_type') == 'alert':
print(f\"[{evt['alert']['severity']}] {evt['alert']['signature']}\")
print(f\" {evt['src_ip']}:{evt.get('src_port','')} -> \
{evt['dest_ip']}:{evt.get('dest_port','')}\")
if 'http' in evt:
print(f\" URL: {evt['http'].get('url','')}\")
print(f\" UA: {evt['http'].get('http_user_agent','')}\")
print()
" 2>/dev/null
Set up Suricata with Emerging Threats rules on a test system:
~~~bash
# Install and update rules
sudo apt install suricata
sudo suricata-update enable-source et/open
sudo suricata-update
# Capture some traffic with tcpdump
sudo tcpdump -i eth0 -c 5000 -w /tmp/capture.pcap
# Analyze the capture with Suricata
sudo suricata -r /tmp/capture.pcap -l /tmp/suricata-output/
# View results
cat /tmp/suricata-output/fast.log
cat /tmp/suricata-output/eve.json | python3 -m json.tool | head -100
# Generate test traffic that triggers rules
# (against YOUR OWN test server only)
curl "http://your-test-server/page?id=1'+UNION+SELECT+1,2,3--"
curl -A "sqlmap/1.7" http://your-test-server/
curl "http://your-test-server/page?file=../../../etc/passwd"
# Check if Suricata detected them
grep "alert" /var/log/suricata/fast.log
~~~
Web Application Firewalls (WAFs)
Everything discussed so far works at the network and transport layers. WAFs work at Layer 7 — they understand HTTP specifically.
WAF Inspection Points
graph TD
subgraph "HTTP Request Inspection"
A["POST /api/users?search=admin HTTP/1.1"] -->|"1. URL path + query string"| CHECK1[SQL injection, path traversal,<br/>parameter tampering]
B["Host: example.com"] -->|"2. Host header"| CHECK2[Host header injection,<br/>virtual host abuse]
C["Cookie: session=abc123"] -->|"3. Cookies"| CHECK3[Session fixation,<br/>cookie injection]
D["Content-Type: application/json"] -->|"4. Content-Type"| CHECK4[Content-type mismatch,<br/>multipart abuse]
E["User-Agent: sqlmap/1.7"] -->|"5. User-Agent"| CHECK5[Known scanner/bot<br/>fingerprints]
F["{\"name\":\"<script>alert(1)</script>\"}"] -->|"6. Request body"| CHECK6[XSS, injection,<br/>XXE, mass assignment]
end
CHECK1 --> DECISION{WAF Decision}
CHECK2 --> DECISION
CHECK3 --> DECISION
CHECK4 --> DECISION
CHECK5 --> DECISION
CHECK6 --> DECISION
DECISION -->|Clean| PASS[Forward to application]
DECISION -->|Malicious| BLOCK[Block + Log + Alert]
style BLOCK fill:#ff6b6b,stroke:#c0392b,color:#fff
style PASS fill:#2ecc71,stroke:#27ae60,color:#fff
WAF Bypass Techniques
During a penetration test, the client proudly announced they had a top-tier cloud WAF. It was bypassed in four different ways within the first hour:
1. **Encoding bypass:** The WAF checked for `<script>` but not double-URL-encoded `%253Cscript%253E` or tag splitting `<scr<script>ipt>`. After the WAF passed it through, the web server decoded the double encoding.
2. **JSON body bypass:** The WAF inspected URL parameters and form bodies but did not parse JSON request bodies. The SQL injection payload was sent as a JSON field value — the WAF saw valid JSON and passed it through.
3. **Chunked transfer encoding:** `UNION SELECT` was split across two HTTP chunks: `UNI` and `ON SELECT`. The WAF inspected each chunk independently and found nothing suspicious. The web server reassembled the chunks before processing.
4. **HTTP/2 header manipulation:** The WAF inspected HTTP/1.1 traffic. A direct HTTP/2 connection with a crafted pseudo-header bypassed the WAF entirely.
The client thought the WAF was their primary defense. It was a speed bump.
| Bypass Technique | How it works | Example |
|---|---|---|
| URL encoding | Encode special characters | %27%20OR%201%3D1 |
| Double encoding | Encode the percent signs | %2527%2520OR |
| Unicode encoding | Use Unicode representations | \u0027 OR |
| Case variation | Mix upper/lowercase | SeLeCt, uNiOn |
| Comment injection | Break keywords with SQL comments | SEL/**/ECT, UN/**/ION |
| Alternative syntax | Use functions instead of keywords | CHAR(83,69,76,69,67,84) for SELECT |
| HTTP parameter pollution | Duplicate parameters | ?id=1&id=UNION+SELECT |
| Chunked encoding | Split payload across chunks | Transfer-Encoding: chunked |
| Protocol mismatch | Use HTTP/2, WebSocket | Direct H2 connection bypassing H1 WAF |
| Multipart abuse | Payload in file upload boundary | Content-Disposition: form-data; name="x'; DROP TABLE--" |
| Newline injection | Break rules with \r\n | SEL\r\nECT |
ModSecurity with OWASP Core Rule Set (CRS)
ModSecurity is the most widely deployed open-source WAF engine:
# Install ModSecurity for Nginx
sudo apt install libmodsecurity3 libmodsecurity-dev
# Download OWASP Core Rule Set
git clone https://github.com/coreruleset/coreruleset.git /etc/modsecurity/crs
cp /etc/modsecurity/crs/crs-setup.conf.example /etc/modsecurity/crs/crs-setup.conf
# Nginx configuration with ModSecurity
load_module modules/ngx_http_modsecurity_module.so;
server {
listen 443 ssl http2;
server_name example.com;
modsecurity on;
modsecurity_rules_file /etc/modsecurity/main.conf;
location / {
proxy_pass http://backend;
}
}
OWASP CRS Paranoia Levels:
graph LR
subgraph "Paranoia Levels"
PL1[Level 1<br/>Default] -->|"More rules"| PL2[Level 2]
PL2 -->|"More rules"| PL3[Level 3]
PL3 -->|"More rules"| PL4[Level 4]
end
PL1 --- D1["Low false positives<br/>Catches obvious attacks<br/>Good starting point"]
PL2 --- D2["Moderate FPs<br/>Catches encoded attacks<br/>Needs some tuning"]
PL3 --- D3["Higher FPs<br/>Catches obfuscated attacks<br/>Significant tuning needed"]
PL4 --- D4["Many FPs<br/>Maximum detection<br/>Only for high-security apps<br/>with extensive tuning"]
style PL1 fill:#2ecc71,stroke:#27ae60,color:#fff
style PL2 fill:#f39c12,stroke:#e67e22,color:#fff
style PL3 fill:#e74c3c,stroke:#c0392b,color:#fff
style PL4 fill:#8e44ad,stroke:#6c3483,color:#fff
WAF deployment best practices:
- Deploy in detection mode first (log, don't block)
- Monitor logs for 2-4 weeks
- Create exclusion rules for legitimate traffic patterns (e.g., admin endpoints that legitimately contain SQL keywords)
- Gradually increase paranoia level
- Switch to blocking mode for high-confidence rules
- Keep some rules in detection-only for continued monitoring
- Review and tune monthly
Defense in Depth: The Complete Traffic Flow
Here is how all these layers work together in a production environment.
flowchart TD
INTERNET[Internet] --> EDGE
subgraph EDGE["Layer 1: Edge / DDoS Protection"]
CF[Cloudflare / AWS Shield]
CF_DESC["Volumetric attack mitigation<br/>IP reputation filtering<br/>Geographic blocking<br/>Bot management"]
end
EDGE --> FW
subgraph FW["Layer 2: Perimeter Firewall (Stateful)"]
FIREWALL[nftables / AWS Security Group]
FW_DESC["Port/protocol filtering<br/>Allow only 80, 443 inbound<br/>Connection state tracking<br/>Anti-spoofing rules"]
end
FW --> IPS_L
subgraph IPS_L["Layer 3: IPS (Suricata, Inline)"]
IPS_E[Suricata IPS Engine]
IPS_DESC["Known exploit signatures<br/>Protocol anomaly detection<br/>TLS/JA3 fingerprinting<br/>Drops matched attacks"]
end
IPS_L --> LB
subgraph LB["Layer 4: Load Balancer / Reverse Proxy"]
NGINX[Nginx / HAProxy / ALB]
LB_DESC["TLS termination<br/>Request routing<br/>Connection rate limiting<br/>Header normalization"]
end
LB --> WAF_L
subgraph WAF_L["Layer 5: WAF"]
MODSEC[ModSecurity + OWASP CRS]
WAF_DESC["SQL injection detection<br/>XSS detection<br/>Command injection detection<br/>Bot/scanner fingerprinting"]
end
WAF_L --> APP
subgraph APP["Layer 6: Application"]
APPSERVER[Application Code]
APP_DESC["Parameterized queries<br/>Output encoding<br/>Input validation<br/>Authorization checks"]
end
APP --> DB
subgraph DB["Layer 7: Database"]
DATABASE[PostgreSQL / MySQL]
DB_DESC["Least-privilege accounts<br/>Query audit logging<br/>Encryption at rest<br/>Row-level security"]
end
SIEM_M["Monitoring: IDS + SIEM + Log Aggregation"]
EDGE -.-> SIEM_M
FW -.-> SIEM_M
IPS_L -.-> SIEM_M
WAF_L -.-> SIEM_M
APP -.-> SIEM_M
DB -.-> SIEM_M
Why do you still need parameterized queries if the WAF catches SQL injection? Because the WAF might catch it. Over a dozen bypass techniques were just demonstrated above. The WAF might be misconfigured, have a rule gap, or face an encoding the signatures don't cover. Parameterized queries are immune to SQL injection — they work at the protocol level and cannot be bypassed regardless of encoding, obfuscation, or creative syntax. The WAF is a safety net. The application is the real defense. Both must be in place.
The principle of defense in depth comes from military fortification: moats, walls, keeps, and citadels. Each layer:
1. **Delays the attacker** — buying time for detection and response
2. **Reduces the attack surface** — each layer filters some attacks
3. **Provides redundancy** — if one layer fails, others still function
4. **Increases attacker cost** — bypassing multiple layers requires more skill, time, and resources
In mathematical terms: if each layer catches 90% of attacks, two layers catch 99%, and three layers catch 99.9%. The residual risk from any single layer is dramatically reduced by adding more layers.
No single layer is perfect. The combination provides security that no individual component can achieve alone. The attacker must bypass ALL layers; the defender only needs ONE layer to catch the attack.
Monitoring, Alerting, and Response
Detection without response is just expensive logging. All these systems must feed into a central monitoring pipeline:
# Suricata EVE → Filebeat → Elasticsearch → Kibana
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/log/suricata/eve.json
json.keys_under_root: true
json.add_error_key: true
output.elasticsearch:
hosts: ["https://elk.internal:9200"]
index: "suricata-%{+yyyy.MM.dd}"
Key metrics to monitor and alert on:
| Layer | Metric | Alert Threshold |
|---|---|---|
| Firewall | Dropped packets/sec | > 10,000 (possible DDoS) |
| Firewall | Connection table utilization | > 80% (table exhaustion risk) |
| IDS/IPS | High-severity alerts/hour | > 0 (investigate immediately) |
| IDS/IPS | Unique source IPs triggering alerts | Sudden spike |
| WAF | Blocked requests/min | > baseline + 3 std dev |
| WAF | SQL injection rule triggers | Any (investigate) |
| Application | Auth failure rate | > 100/min per IP |
| Application | 500 error rate | > 1% of requests |
What You've Learned
This chapter covered the layered network defense systems that protect traffic from the internet to the application:
-
Packet filtering firewalls operate at Layers 3-4, filtering by IP, port, and protocol. They are fast but cannot inspect application content or track connection state.
-
Stateful inspection firewalls track TCP connection state via a state table, automatically allowing return traffic for established sessions and preventing spoofed packets.
-
iptables/nftables are the Linux kernel firewall tools. The chain traversal path (PREROUTING -> INPUT/FORWARD -> OUTPUT -> POSTROUTING) determines when and how rules are applied. Design rules with default deny, explicit allows, and rate limiting.
-
IDS (Snort, Suricata, Zeek) monitors traffic and generates alerts using signature-based rules and anomaly detection. It is passive — it copies traffic via a TAP or mirror port. Suricata's EVE JSON logging provides rich structured data for SIEM integration.
-
IPS sits inline and actively blocks traffic matching attack signatures. It must be carefully tuned — start in IDS mode, eliminate false positives, then enable blocking for high-confidence rules.
-
WAFs inspect HTTP traffic specifically, catching SQL injection, XSS, and other web application attacks. They can be bypassed through encoding, chunked transfer, protocol tricks, and obfuscation. They are a safety net, not a primary defense.
-
Defense in depth layers these systems so that each catches what the others miss. No single layer is sufficient. The attacker must bypass all layers; the defender needs only one to succeed.
Firewalls control what traffic enters, IDS/IPS identifies malicious traffic, WAFs inspect web-specific attacks, and the application handles business logic security. Each one has blind spots, so you need all of them — and you need to monitor all of them. The most sophisticated security stack in the world is useless if nobody is watching the alerts. Detection without response is just expensive logging.