Firewall #
A firewall is a defense layer filtering network traffic based on predefined rules — deciding what may enter, what may leave, and what must be blocked. In the context of modern web applications, a “firewall” is no longer just a physical box in a server rack — it encompasses cloud security groups, iptables on Linux, Web Application Firewalls (WAFs) in front of applications, and even rate limiting configurations on load balancers.
Understanding firewalls from a web developer’s perspective means understanding how traffic flows to and from applications, at which points filtering rules are applied, and how various firewall layers work together to provide defense in depth. A correctly configured firewall is the difference between a system exposing only what’s needed vs a system letting attackers explore freely.
The Layered Defense Model #
No single firewall component can protect against all threats. Defense in depth uses several complementary layers.
graph TB
Internet --> CDN["CDN / DDoS Protection\nCloudflare, AWS Shield"]
CDN --> WAF["Web Application Firewall\nOWASP rules, rate limiting"]
WAF --> LB["Load Balancer\nSSL termination, health checks"]
LB --> SG["Security Group / Network Firewall\nPort whitelist, IP rules"]
SG --> App["Application Server\niptables, application"]
App --> DB["Database\nOnly from App Server"]
App --> Cache["Redis/Cache\nOnly from App Server"]
App --> Queue["Message Queue\nOnly from App Server"]
style CDN fill:#e8f4f8
style WAF fill:#e8f4f8
style LB fill:#e8f8e8
style SG fill:#e8f8e8
style App fill:#f8f8e8
style DB fill:#f8e8e8
style Cache fill:#f8e8e8Each layer has different responsibilities:
Layers and their responsibilities:
CDN/DDoS Protection:
→ Absorbs volumetric attacks before reaching infrastructure
→ Anycast routing distributes traffic to edge nodes
→ IP reputation filtering based on global databases
Web Application Firewall (WAF):
→ Filters requests based on known attack patterns
→ OWASP Core Rule Set: SQLi, XSS, RCE payloads
→ Rate limiting per IP and per endpoint
Load Balancer:
→ SSL/TLS termination
→ Health checks — traffic not forwarded to down servers
→ Connection limiting
Security Group / Network Firewall:
→ Port whitelists — only needed ports open
→ Source IP restrictions for sensitive ports (SSH, databases)
→ Network segmentation between tiers
Host-based Firewall (iptables/nftables):
→ Defense in depth at the server level
→ Filters even if security groups are misconfigured
→ Egress filtering limiting outbound connections
Database / Cache:
→ Only accepts connections from the application tier
→ Never open to the internet
Network Firewalls: iptables and Security Groups #
iptables on Linux #
iptables is the host-based firewall running on every Linux server. It filters packets based on chains and defined rules.
#!/bin/bash
# firewall-setup.sh — iptables configuration for an application server
# Flush all existing rules
iptables -F
iptables -X
iptables -Z
# Default policy: DROP all traffic (deny by default)
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT # Allow all outgoing (tightened later)
# =============================================
# LOOPBACK — allow internal traffic
# =============================================
iptables -A INPUT -i lo -j ACCEPT
# =============================================
# ESTABLISHED CONNECTIONS — allow responses
# =============================================
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# =============================================
# ICMP — allow ping (but limit to avoid floods)
# =============================================
iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 1/second --limit-burst 10 -j ACCEPT
# =============================================
# SSH — allow from admin IPs only (DON'T leave open to the internet!)
# =============================================
iptables -A INPUT -p tcp --dport 22 \
-s 10.0.0.0/8 -j ACCEPT # from the internal network only
# iptables -A INPUT -p tcp --dport 22 \
# -s 203.x.x.x/32 -j ACCEPT # or from specific admin IPs
# =============================================
# HTTPS and HTTP — allow from anywhere
# =============================================
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # for redirects to HTTPS
# =============================================
# RATE LIMITING — anti brute force and DDoS
# =============================================
# Limit new connections per IP (anti port scanning and excessive connections)
iptables -A INPUT -p tcp --dport 443 \
-m conntrack --ctstate NEW \
-m limit --limit 60/minute --limit-burst 20 -j ACCEPT
# =============================================
# DROP the rest and log for investigation
# =============================================
iptables -A INPUT -j LOG --log-prefix "IPTABLES_DROP: " \
--log-level 4 -m limit --limit 10/minute # limit logging
iptables -A INPUT -j DROP
# =============================================
# EGRESS FILTERING — limit outbound connections
# =============================================
# This is important for detecting and limiting compromised servers
iptables -P OUTPUT DROP
# DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
# NTP (time sync)
iptables -A OUTPUT -p udp --dport 123 -j ACCEPT
# HTTP/HTTPS for downloading updates and external APIs
iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
# Connections to internal databases (e.g. Redis at 10.0.1.5)
iptables -A OUTPUT -p tcp -d 10.0.1.5 --dport 6379 -j ACCEPT
# Connections to internal PostgreSQL (e.g. 10.0.1.10)
iptables -A OUTPUT -p tcp -d 10.0.1.10 --dport 5432 -j ACCEPT
# Established connections (responses)
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Loopback
iptables -A OUTPUT -o lo -j ACCEPT
# Log and drop unauthorized egress
iptables -A OUTPUT -j LOG --log-prefix "EGRESS_DROP: " --log-level 4 \
-m limit --limit 5/minute
iptables -A OUTPUT -j DROP
# Save the rules to persist after reboots
iptables-save > /etc/iptables/rules.v4
Security Groups in the Cloud (AWS as an example) #
Cloud security groups work at the virtual network level — more flexible than iptables because they can be updated without entering servers.
# Terraform — Security Group for an application server
resource "aws_security_group" "app_server" {
name = "app-server-sg"
description = "Security group for the application server"
vpc_id = aws_vpc.main.id
# Ingress: traffic entering the server
# HTTPS from the load balancer only (not directly from the internet)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.load_balancer.id]
description = "HTTPS from the load balancer"
}
# HTTP from the load balancer (for redirects to HTTPS)
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.load_balancer.id]
description = "HTTP redirect from the load balancer"
}
# SSH only from bastion hosts or VPNs (DON'T use 0.0.0.0/0)
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
description = "SSH from the bastion host only"
}
# Egress: traffic leaving the server
# To the database (RDS)
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.database.id]
description = "PostgreSQL to RDS"
}
# To Redis (ElastiCache)
egress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.redis.id]
description = "Redis to ElastiCache"
}
# HTTPS to the internet (for external APIs, updates)
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS to the internet"
}
# DNS
egress {
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
description = "DNS resolution"
}
tags = {
Name = "app-server-sg"
Environment = "production"
}
}
# Security group for the database — NOT from the internet
resource "aws_security_group" "database" {
name = "database-sg"
description = "Database only accessible from the app server"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app_server.id]
description = "PostgreSQL from the app server only"
}
# No egress to the internet from the database
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["10.0.0.0/8"] # internal network only
description = "Internal network only"
}
}
Web Application Firewalls (WAFs) #
WAFs operate at layer 7 (the application layer) — they understand HTTP and can filter based on request content, not just IPs and ports.
# ModSecurity in Nginx — an open source WAF
# In nginx.conf:
# modsecurity on;
# modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
# modsecurity.conf — basic configuration
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecRequestBodyLimit 13107200 # 12.5MB max request body
# OWASP Core Rule Set — rules for well-known attacks
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf
# SQL injection detection
SecRule ARGS "@detectSQLi" \
"id:1001,phase:2,deny,status:403,log,msg:'SQL Injection Attempt'"
# XSS detection
SecRule ARGS "@detectXSS" \
"id:1002,phase:2,deny,status:403,log,msg:'XSS Attempt'"
# Path traversal detection
SecRule REQUEST_URI "@contains ../" \
"id:1003,phase:1,deny,status:403,log,msg:'Path Traversal Attempt'"
# Per-IP rate limiting for login endpoints
SecRule REQUEST_URI "@beginsWith /login" \
"id:1010,phase:1,pass,nolog,setvar:ip.login_count=+1,\
expirevar:ip.login_count=300"
SecRule IP:LOGIN_COUNT "@gt 20" \
"id:1011,phase:1,deny,status:429,log,msg:'Login rate limit exceeded'"
WAFs in the Cloud (AWS WAF as an example) #
# AWS WAF v2 with Managed Rule Groups
resource "aws_wafv2_web_acl" "main" {
name = "main-waf"
scope = "REGIONAL" # or CLOUDFRONT for CDNs
default_action {
allow {}
}
# AWS Managed Rules — OWASP protection
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 1
override_action {
none {} # use the managed rule's action
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CommonRuleSetMetric"
sampled_requests_enabled = true
}
}
# SQL injection protection
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SQLiRuleSetMetric"
sampled_requests_enabled = true
}
}
# Per-IP rate limiting
rule {
name = "RateLimitPerIP"
priority = 10
action {
block {}
}
statement {
rate_based_statement {
limit = 2000 # requests per 5 minutes
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitMetric"
sampled_requests_enabled = true
}
}
# IP whitelist for admin endpoints
rule {
name = "AdminIPWhitelist"
priority = 5
action {
block {}
}
statement {
and_statement {
statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "STARTS_WITH"
search_string = "/admin"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
statement {
not_statement {
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.admin_ips.arn
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AdminAccessMetric"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "MainWAFMetric"
sampled_requests_enabled = true
}
}
# IP set for the admin whitelist
resource "aws_wafv2_ip_set" "admin_ips" {
name = "admin-ip-whitelist"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = [
"203.x.x.x/32", # office IPs
"10.0.0.0/8", # internal VPN
]
}
IP Management: Allowlists and Blocklists #
// Allowlist and blocklist management in applications
import (
"context"
"net"
"net/http"
"time"
"github.com/redis/go-redis/v9"
)
var redisClient = redis.NewClient(&redis.Options{Addr: "redis:6379"})
type IPManager struct{}
// Check whether an IP is blocked.
func (IPManager) isBlocked(ctx context.Context, ip string) bool {
// Check the permanent blocklist
blocked, _ := redisClient.SIsMember(ctx, "ip:blocklist", ip).Result()
if blocked {
return true
}
// Check temporary blocks
tempKey := "ip:temp_block:" + ip
exists, _ := redisClient.Exists(ctx, tempKey).Result()
if exists == 1 {
return true
}
// Check the subnet blocklist (for handling IP ranges)
blockedSubnets, _ := redisClient.SMembers(ctx, "ip:blocklist:subnets").Result()
ipObj := net.ParseIP(ip)
for _, subnet := range blockedSubnets {
_, network, err := net.ParseCIDR(subnet)
if err == nil && network.Contains(ipObj) {
return true
}
}
return false
}
// Check whether an IP is in the allowlist (highest priority).
func (IPManager) isAllowed(ctx context.Context, ip string) bool {
allowed, _ := redisClient.SIsMember(ctx, "ip:allowlist", ip).Result()
return allowed
}
// Temporarily block an IP.
func (IPManager) blockTemporarily(ctx context.Context, ip string, seconds time.Duration, reason string) {
redisClient.Set(ctx, "ip:temp_block:"+ip, reason, seconds)
}
// Permanently block an IP.
func (IPManager) blockPermanently(ctx context.Context, ip string, reason string) {
pipe := redisClient.TxPipeline()
pipe.SAdd(ctx, "ip:blocklist", ip)
pipe.HSet(ctx, "ip:blocklist:reasons", ip, reason)
pipe.Exec(ctx)
}
// Remove an IP from all blocklists.
func (IPManager) unblock(ctx context.Context, ip string) {
redisClient.SRem(ctx, "ip:blocklist", ip)
redisClient.Del(ctx, "ip:temp_block:"+ip)
}
var ipManager = IPManager{}
// Middleware applying IP management
func checkIP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
// Allowlist entries are always allowed (monitoring, internal tools)
if ipManager.isAllowed(r.Context(), ip) {
next.ServeHTTP(w, r)
return
}
// Blocklist entries are blocked
if ipManager.isBlocked(r.Context(), ip) {
http.Error(w, `{"error": "Access denied"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
Egress Filtering: Limiting Outbound Connections #
Egress filtering is often neglected — the focus is usually on limiting incoming traffic, but uncontrolled outbound traffic can indicate compromised servers or data leaks.
# Egress filtering with iptables — only allow needed outbound connections
# Change the default OUTPUT policy
iptables -P OUTPUT DROP
# Loopback always allowed
iptables -A OUTPUT -o lo -j ACCEPT
# Established connections (responses to incoming requests)
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# DNS — needed for name resolution
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
# NTP — time synchronization
iptables -A OUTPUT -p udp --dport 123 -j ACCEPT
# HTTPS to the internet — for OS updates, dependency downloads, external APIs
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
# Internal databases only (not the internet)
iptables -A OUTPUT -p tcp -d 10.0.1.0/24 --dport 5432 -j ACCEPT
iptables -A OUTPUT -p tcp -d 10.0.1.0/24 --dport 6379 -j ACCEPT
# SMTP for sending email (if there's an outbound email server)
iptables -A OUTPUT -p tcp -d 10.0.2.5 --dport 587 -j ACCEPT
# Log blocked outbound connections — detects compromised servers
iptables -A OUTPUT -j LOG --log-prefix "EGRESS_BLOCKED: " \
--log-level 4 -m limit --limit 5/minute
iptables -A OUTPUT -j DROP
Why egress filtering matters:
If a server is compromised, attackers will try:
→ Callbacks to C2 servers (command and control)
→ Data exfiltration — sending data to attacker servers
→ Downloading additional post-exploitation tools
→ Pivoting to other internal systems
With egress filtering:
→ Connections to attacker domains blocked → C2 can't run
→ Data exfiltration to the internet blocked
→ Alerts triggered because unauthorized connections appear
Suddenly appearing "EGRESS_BLOCKED" logs are warning signs
requiring immediate investigation
Firewall Monitoring #
Firewall rules that exist but aren’t monitored are useless rules.
// Parsing and analyzing iptables logs to detect anomalies
import (
"bufio"
"os"
"regexp"
"strconv"
)
// Parse one iptables log line.
// Format: Jun 15 14:23:45 server IPTABLES_DROP: IN=eth0 OUT= SRC=1.2.3.4 DST=10.0.0.1 ...
var logPattern = regexp.MustCompile(`(\w+ \d+ \d+:\d+:\d+) (\S+) ([\w_]+): IN=\S* OUT=\S* SRC=(\S+) DST=(\S+) (?:.*)?DPT=(\d+)`)
type LogEntry struct {
Prefix string
SrcIP string
DstIP string
DstPort int
}
func parseIptablesLog(logLine string) *LogEntry {
match := logPattern.FindStringSubmatch(logLine)
if match == nil {
return nil
}
port, _ := strconv.Atoi(match[6])
return &LogEntry{Prefix: match[3], SrcIP: match[4], DstIP: match[5], DstPort: port}
}
type FirewallAnalyzer struct {
blockedIPs map[string]int
blockedPorts map[int]int
egressBlocked map[string][]LogEntry
}
func NewFirewallAnalyzer() *FirewallAnalyzer {
return &FirewallAnalyzer{
blockedIPs: make(map[string]int),
blockedPorts: make(map[int]int),
egressBlocked: make(map[string][]LogEntry),
}
}
func (a *FirewallAnalyzer) analyzeLogFile(logPath string) error {
file, err := os.Open(logPath)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
entry := parseIptablesLog(scanner.Text())
if entry == nil {
continue
}
if entry.Prefix == "IPTABLES_DROP" {
// Blocked incoming traffic
a.blockedIPs[entry.SrcIP]++
a.blockedPorts[entry.DstPort]++
} else if entry.Prefix == "EGRESS_BLOCKED" {
// Blocked outbound traffic — needs investigation!
a.egressBlocked[entry.DstIP] = append(a.egressBlocked[entry.DstIP], *entry)
}
}
return scanner.Err()
}
func (a *FirewallAnalyzer) getAlerts() []map[string]interface{} {
alerts := []map[string]interface{}{}
// Alert: frequently blocked IPs (could be scanners or attackers)
for ip, count := range a.blockedIPs {
if count > 100 {
alerts = append(alerts, map[string]interface{}{
"type": "frequent_blocked_ip",
"ip": ip,
"count": count,
"action": "consider_permanent_block",
})
}
}
// Alert: blocked egress (could be a compromised server)
if len(a.egressBlocked) > 0 {
alerts = append(alerts, map[string]interface{}{
"type": "egress_blocked",
"destinations": a.egressBlocked,
"action": "INVESTIGATE_IMMEDIATELY", // this is serious
})
}
return alerts
}
Fail2ban Configuration: Automated IP Blocking #
Fail2ban monitors application logs and automatically blocks IPs showing dangerous behavior.
# /etc/fail2ban/jail.local
[DEFAULT]
# Default ban of 1 hour
bantime = 3600
# Check the last 10 minutes
findtime = 600
# 5 attempts before banning
maxretry = 5
# Backend for log monitoring
backend = auto
# Email notifications (optional)
# action = %(action_mw)s
destemail = [email protected]
sendername = Fail2Ban
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 86400 # 24 hours for SSH brute force
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 300
# Custom filter for the application
[app-login-brute]
enabled = true
port = http,https
filter = app-login-brute
logpath = /var/log/app/security.log
maxretry = 5
findtime = 300
bantime = 1800 # 30 minutes
# /etc/fail2ban/filter.d/app-login-brute.conf
[Definition]
# Match log entries for failed logins
failregex = ^.*"event":"login_failed".*"ip":"<HOST>".*$
# Ignored
ignoreregex =
Anti-Patterns to Avoid #
# ✗ Anti-pattern 1: SSH open to the entire internet
# Security groups / iptables:
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # DON'T!
# AWS: ingress port 22 from 0.0.0.0/0
# ✓ Solution: SSH only from specific IPs or VPNs
iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
# Or use bastion hosts / jump servers
# ✗ Anti-pattern 2: database ports open to the internet
# PostgreSQL port 5432, MySQL port 3306 from 0.0.0.0/0
# Attackers can directly try brute forcing databases
# ✓ Solution: databases only accept from application servers
# Database security groups: ingress 5432 from app-server-sg only
# ✗ Anti-pattern 3: no egress filtering
# Compromised servers → attackers freely download tools and send data out
# ✓ Solution: whitelist needed egress, block the rest
# ✗ Anti-pattern 4: WAFs in "detection only" mode never enforced
# WAFs logging but not blocking — like unmonitored CCTV
# ✓ Solution: after a monitoring and tuning period, enable blocking mode
# ✗ Anti-pattern 5: firewall rules never reviewed
# Rules added for temporary needs and never removed
# Firewall creep: more exceptions = weaker protection
# ✓ Solution: review firewall rules periodically (quarterly)
# Remove rules no longer needed
# ✗ Anti-pattern 6: no alerting for blocked traffic
# Attacks happen but nobody knows
# ✓ Solution: monitor and alert on blocked-traffic spikes
Firewall Checklist #
NETWORK FIREWALLS:
□ Default DENY policies — only needed ports open
□ SSH not open to the internet (0.0.0.0/0)
□ Database ports not open to the internet
□ Admin/management ports only from VPNs or bastion hosts
□ Egress filtering configured — only needed traffic allowed
SECURITY GROUPS (CLOUD):
□ Each tier has its own security group
□ Databases only accept from app server security groups
□ Cache/Redis only accepts from app server security groups
□ Load balancers are the only thing exposed to the internet
□ No security groups with 0.0.0.0/0 for SSH or databases
WEB APPLICATION FIREWALLS:
□ WAFs active in front of applications
□ OWASP Core Rule Set configured
□ WAFs in blocking mode (not just detection)
□ Rate limiting configured at WAFs
□ False positives monitored and rules tuned periodically
IP MANAGEMENT:
□ Blocklists for known-dangerous IPs
□ Allowlists for internal access and monitoring
□ Fail2ban or equivalents for automated blocking
□ Processes for reviewing and updating blocklists
MONITORING:
□ Firewall logs collected and analyzed
□ Alerts for blocked-traffic spikes
□ Alerts for blocked egress (compromise indications)
□ Dashboards for traffic patterns
□ Adequate log retention for forensic investigations
REVIEW & MAINTENANCE:
□ Firewall rules reviewed periodically (at least quarterly)
□ Unneeded rules removed
□ Firewall changes through review processes (IaC, PRs, approvals)
□ Documentation for every exception and its reason
Summary #
- Defense in depth uses multiple layers — CDN/DDoS protection, WAFs, load balancers, security groups, and host-based firewalls work together. Each layer catches what the previous layer missed.
- Default DENY is the fundamental principle — only explicitly allowed traffic may pass. Everything else is blocked by default.
- SSH must never be open to the internet — use bastion hosts, VPNs, or strict IP allowlists. Port 22 open to 0.0.0.0/0 is an invitation to brute force.
- Databases must never be open to the internet — only application servers may communicate with databases. Correct security groups ensure this.
- Egress filtering is as important as ingress — unauthorized outbound traffic is a sign of compromised servers. Whitelist egress and alert on anomalies.
- WAFs operate at the application layer — they understand HTTP and can detect attack payloads (SQLi, XSS, path traversal) that regular network firewalls can’t filter.
- WAFs must be in blocking mode — detection-only WAFs provide no real protection. After tuning to reduce false positives, enable blocking.
- Fail2ban automates responses to attacks — SSH brute force, repeated failed logins, and other dangerous patterns automatically produce temporary IP bans.
- Firewall rules need periodic reviews — rules added for temporary needs and never removed create unintentional attack surfaces.
- All firewall changes must go through Infrastructure as Code — manually configured iptables or security groups are undocumented and unreviewable. Use Terraform or CloudFormation.
#
- Defense in depth uses multiple layers — CDN/DDoS protection, WAFs, load balancers, security groups, and host-based firewalls work together. Each layer catches what the previous layer missed.
- Default DENY is the fundamental principle — only explicitly allowed traffic may pass. Everything else is blocked by default.
- SSH must never be open to the internet — use bastion hosts, VPNs, or strict IP allowlists. Port 22 open to 0.0.0.0/0 is an invitation to brute force.
- Databases must never be open to the internet — only application servers may communicate with databases. Correct security groups ensure this.
- Egress filtering is as important as ingress — unauthorized outbound traffic is a sign of compromised servers. Whitelist egress and alert on anomalies.
- WAFs operate at the application layer — they understand HTTP and can detect attack payloads (SQLi, XSS, path traversal) that regular network firewalls can’t filter.
- WAFs must be in blocking mode — detection-only WAFs provide no real protection. After tuning to reduce false positives, enable blocking.
- Fail2ban automates responses to attacks — SSH brute force, repeated failed logins, and other dangerous patterns automatically produce temporary IP bans.
- Firewall rules need periodic reviews — rules added for temporary needs and never removed create unintentional attack surfaces.
- All firewall changes must go through Infrastructure as Code — manually configured iptables or security groups are undocumented and unreviewable. Use Terraform or CloudFormation.