
Configuring Firewalls
- 76 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
Configuring-firewalls is a Claude Code skill that configures host-based firewalls and cloud security groups with practical rules and lockout-safe patterns.
About
Configuring-firewalls is a Claude Code skill for configuring host-based firewalls (iptables, nftables, UFW) and cloud security groups (AWS, GCP, Azure). A developer uses it when exposing services, hardening servers, or implementing network segmentation. It provides a tool-selection decision framework, quick-start rule examples, and safety patterns that prevent lockouts, plus Kubernetes NetworkPolicy guidance.
- Host-based firewalls (iptables, nftables, UFW, firewalld)
- Cloud security groups for AWS, GCP, and Azure
- Lockout-safe patterns and Kubernetes NetworkPolicies
Configuring Firewalls by the numbers
- 76 all-time installs (skills.sh)
- Ranked #632 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
configuring-firewalls capabilities & compatibility
- Capabilities
- configuring firewalls · configuring nginx · deploying on aws · deploying on azure
- Works with
- aws · gcp · azure · kubernetes · terraform
- Use cases
- devops · security audit
- Platforms
- Linux
What configuring-firewalls says it does
Configure host-based firewalls (iptables, nftables, UFW) and cloud security groups (AWS, GCP, Azure) with practical rules
CRITICAL: Allow SSH before enabling (prevent lockout)
npx skills add https://github.com/ancoleman/ai-design-components --skill configuring-firewallsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 76 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Configure a host or cloud firewall to expose a service safely without locking yourself out.
Who is it for?
Hardening servers and exposing services with host or cloud firewall rules.
Skip if: Application-layer authentication or WAF rules.
When should I use this skill?
Setting up a firewall, security group, or network segmentation for a server or service.
What you get
Safe, defense-in-depth firewall rules that prevent lockouts and misconfigurations.
- Firewall tool selection
- UFW/nftables/iptables rule sets
- Cloud security group config
By the numbers
- nftables O(log n) performance vs iptables O(n)
Files
Configuring Firewalls
Purpose
Guide engineers through configuring firewalls across host-based (iptables, nftables, UFW), cloud-based (AWS Security Groups, NACLs), and container-based (Kubernetes NetworkPolicies) environments with practical rule examples and safety patterns to prevent lockouts and security misconfigurations.
When to Use This Skill
Trigger Phrases:
- "Configure firewall for [server/service]"
- "Set up security groups for [AWS resource]"
- "Allow port [X] through firewall"
- "Block IP address [X.X.X.X]"
- "Set up UFW on Ubuntu server"
- "Create iptables/nftables rules"
- "Configure bastion host firewall"
- "Implement egress filtering"
Common Scenarios:
- Initial server setup and hardening
- Exposing a new service (web server, API, database)
- Implementing network segmentation
- Creating bastion host or jump box
- Migrating from iptables to nftables
- Configuring cloud security groups
- Troubleshooting connectivity issues
Decision Framework: Which Firewall Tool?
Cloud Environments
AWS:
- Instance-level control → Security Groups (stateful, allow-only rules)
- Subnet-level enforcement → Network ACLs (stateless, allow + deny rules)
- Use both for defense-in-depth
GCP:
- Use VPC Firewall Rules (stateful, priority-based)
Azure:
- Use Network Security Groups (NSGs) (stateful, priority-based)
Host-Based Linux Firewalls
Ubuntu/Debian + Simplicity:
- Use UFW (Uncomplicated Firewall) - recommended for most users
- Front-end for iptables/nftables with simplified syntax
RHEL/CentOS/Fedora:
- Use firewalld (default on Red Hat ecosystem)
- Zone-based configuration with dynamic updates
Modern Distro + Advanced Control:
- Use nftables (best performance, modern standard)
- O(log n) performance vs iptables O(n)
- Unified IPv4/IPv6/NAT syntax
Legacy Systems:
- Use iptables (migrate to nftables when feasible)
- Required for older kernels (< 4.14)
Kubernetes/Containers
- Use NetworkPolicies (requires CNI plugin: Calico, Cilium, Weave)
- See references/k8s-networkpolicies.md
Stateful vs Stateless
Stateful (recommended for most cases):
- Automatically allows return traffic
- Simpler configuration
- Examples: Security Groups, UFW, nftables default
Stateless (specialized use):
- Must explicitly allow both directions
- Fine-grained control, less state tracking
- Examples: Network ACLs, custom nftables rules
Quick Start Examples
UFW (Ubuntu/Debian)
# 1. Set defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 2. CRITICAL: Allow SSH before enabling (prevent lockout)
sudo ufw allow ssh
sudo ufw limit ssh # Rate-limit to prevent brute force
# 3. Allow web traffic
sudo ufw allow http # Port 80
sudo ufw allow https # Port 443
# 4. Allow from specific IP (e.g., database access)
sudo ufw allow from 192.168.1.100 to any port 5432
# 5. Enable firewall
sudo ufw enable
# 6. Verify rules
sudo ufw status verboseFor complete UFW patterns, see references/ufw-patterns.md
nftables (Modern Linux)
#!/usr/sbin/nft -f
# /etc/nftables.conf
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Accept loopback
iif "lo" accept
# Accept established connections (stateful)
ct state established,related accept
# Drop invalid packets
ct state invalid drop
# Allow SSH
tcp dport 22 accept
# Allow HTTP/HTTPS
tcp dport { 80, 443 } accept
# Log dropped packets
log prefix "nftables-drop: " drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}Apply: sudo nft -f /etc/nftables.conf Enable on boot: sudo systemctl enable nftables
For advanced patterns (sets, maps), see references/nftables-patterns.md
AWS Security Groups (Terraform)
# Web server security group
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
# Allow HTTP/HTTPS from anywhere
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow SSH from bastion only
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Allow all outbound
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-server-sg"
}
}For Security Groups vs NACLs guide, see references/aws-security-groups.md
Safety Checklist
Before enabling any firewall:
- [ ] Always allow SSH before enabling (prevent lockout)
- [ ] Test rules before enabling (dry-run when possible)
- [ ] Enable logging for debugging
- [ ] Document rules in version control (Git)
- [ ] Verify externally with nmap:
nmap -Pn <server-ip> - [ ] Have console access (cloud) or physical access (on-prem)
- [ ] Start with default deny, explicitly allow required traffic
- [ ] Use rate limiting for SSH (
ufw limit ssh)
Common Patterns
Pattern 1: Basic Web Server
Requirements:
- Allow HTTP (80) and HTTPS (443) from anywhere
- Allow SSH from specific IP or bastion only
- Default deny all other inbound traffic
UFW:
sudo ufw default deny incoming
sudo ufw allow from 203.0.113.0/24 to any port 22 # Office IP
sudo ufw allow http
sudo ufw allow https
sudo ufw enablenftables: See references/nftables-patterns.md for complete example
AWS Security Group: See references/aws-security-groups.md for Terraform module
Pattern 2: Database Server (Private)
Requirements:
- Allow database port (5432, 3306, etc.) from app tier only
- No public internet access
- SSH from bastion only
See references/database-patterns.md for implementation
Pattern 3: Bastion Host (Jump Box)
Purpose: Single hardened entry point for SSH access
See references/bastion-pattern.md for complete implementation
Pattern 4: Egress Filtering
Purpose: Control outbound traffic to prevent data exfiltration
See references/egress-filtering.md for implementation
Key Concepts
Stateful Firewalls
Track connection state (established, related, new):
- Automatically allow return traffic
- Simpler rule configuration
- Used by: Security Groups, UFW, nftables (default)
Stateless Firewalls
No connection tracking:
- Must explicitly allow both directions
- Must allow ephemeral ports (1024-65535) for return traffic
- Used by: Network ACLs
Defense-in-Depth
Layer multiple firewall controls:
- Cloud: Security Groups + NACLs
- Host: UFW/nftables + fail2ban
- Container: NetworkPolicies
Rule Evaluation
Security Groups (AWS): All rules evaluated, most permissive wins Network ACLs (AWS): Sequential evaluation, first match wins nftables/iptables: Sequential, first match wins UFW: Sequential by rule number
Universal Best Practices
1. Default Deny: Start with deny-all, explicitly allow required traffic 2. Principle of Least Privilege: Only open necessary ports/IPs 3. No 0.0.0.0/0 on Sensitive Ports: Never allow SSH/RDP/database from anywhere 4. Version Control: Store firewall rules in Git 5. Logging: Enable and monitor firewall logs 6. Regular Audits: Review rules quarterly, remove unused 7. Don't Mix Tools: Avoid running iptables and nftables simultaneously 8. Test Before Production: Use staging environment first
Advanced Topics
Bastion Host Architecture: See references/bastion-pattern.md for single entry point patterns
DMZ (Demilitarized Zone): See references/dmz-pattern.md for network segmentation
Egress Filtering: See references/egress-filtering.md for outbound traffic control
Kubernetes NetworkPolicies: See references/k8s-networkpolicies.md for pod-to-pod isolation
Migrating iptables to nftables: See references/migration-guide.md for conversion process
Cloud Firewall Comparisons:
- AWS: references/aws-security-groups.md
- GCP: references/gcp-firewall.md
- Azure: references/azure-nsg.md
Troubleshooting
"I locked myself out via SSH":
- Cloud: Use console/session manager to access
- On-prem: Physical console access or IPMI/iLO
- Prevention: Always allow SSH before enabling firewall
Connection timeouts:
- Check if firewall is blocking traffic:
sudo ufw statusorsudo nft list ruleset - Verify service is listening:
ss -tuln | grep <port> - Test externally:
nmap -Pn <ip> -p <port> - Check logs:
/var/log/ufw.logorjournalctl -u nftables
AWS: Ephemeral port issues:
- NACLs need return traffic: Allow 1024-65535 inbound
- Security Groups are stateful (no ephemeral config needed)
Kubernetes pods can't communicate:
- Check NetworkPolicies:
kubectl get networkpolicies -n <namespace> - Verify CNI plugin supports NetworkPolicies (Calico, Cilium)
- Test without policies first
For complete troubleshooting guide, see references/troubleshooting.md
Common Mistakes to Avoid
❌ Allowing 0.0.0.0/0 on SSH/RDP → Use bastion or VPN ❌ Forgetting to enable firewall → Rules configured but not active ❌ Not testing before enabling → Risk of lockout ❌ Missing ephemeral ports in NACLs → Return traffic blocked ❌ Running iptables + nftables → Conflicts and unpredictable behavior ❌ No logging → Can't debug or audit ❌ Large port ranges → Unnecessary attack surface ❌ Not documenting rules → Future confusion
Tool-Specific Commands
UFW
# Status
sudo ufw status verbose
sudo ufw status numbered
# Add rules
sudo ufw allow <port>/<protocol>
sudo ufw allow from <ip> to any port <port>
sudo ufw limit ssh # Rate limiting
# Delete rules
sudo ufw delete <rule-number>
sudo ufw delete allow 80/tcp
# Logging
sudo ufw logging on
tail -f /var/log/ufw.log
# Reset (disable and remove all rules)
sudo ufw resetnftables
# List ruleset
sudo nft list ruleset
# Load config
sudo nft -f /etc/nftables.conf
# Flush all rules
sudo nft flush ruleset
# Add rule dynamically
sudo nft add rule inet filter input tcp dport 8080 accept
# Enable on boot
sudo systemctl enable nftablesiptables
# List rules
sudo iptables -L -v -n
sudo iptables -L INPUT --line-numbers
# Add rule
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# Delete rule
sudo iptables -D INPUT <rule-number>
# Save rules
sudo netfilter-persistent save # Debian/Ubuntu
sudo service iptables save # RHEL/CentOSAWS CLI
# List security groups
aws ec2 describe-security-groups --group-ids sg-xxxxx
# List NACLs
aws ec2 describe-network-acls --network-acl-ids acl-xxxxx
# Add rule to security group
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxx \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0For infrastructure as code approach, use Terraform (see references/aws-security-groups.md)
Examples Directory
Complete working examples available in:
examples/ufw/- UFW configuration scriptsexamples/nftables/- nftables rulesetsexamples/iptables/- iptables rule scriptsexamples/terraform-aws/- AWS Security Groups and NACLsexamples/terraform-gcp/- GCP firewall rulesexamples/terraform-azure/- Azure NSGsexamples/kubernetes/- NetworkPolicy manifests
Integration Points
Related Skills:
- security-hardening - Firewalls are one component of server hardening. See security-hardening skill for SSH hardening, fail2ban, auditd, and SELinux.
- building-ci-pipelines - CI runners need network access to repos and artifact stores. Configure firewall rules for self-hosted runners.
- deploying-applications - Applications need firewall rules for service exposure. See deploying-applications for integration.
- infrastructure-as-code - Manage firewalls as code with Terraform/CloudFormation. See infrastructure-as-code for IaC best practices.
- kubernetes-operations - Advanced K8s networking beyond basic NetworkPolicies. See kubernetes-operations for Services, Ingress, and CNI configuration.
- network-architecture - Broader network design patterns. See network-architecture for VPC design, subnets, and routing.
Reference Files
Tool-Specific Guides:
- references/ufw-patterns.md - Complete UFW guide with examples
- references/nftables-patterns.md - nftables syntax, sets, maps, logging
- references/iptables-patterns.md - iptables basics and migration path
- references/migration-guide.md - Convert iptables to nftables
Cloud Provider Guides:
- references/aws-security-groups.md - Security Groups vs NACLs with Terraform
- references/gcp-firewall.md - GCP VPC firewall rules
- references/azure-nsg.md - Azure Network Security Groups
Advanced Patterns:
- references/bastion-pattern.md - Jump box architecture
- references/dmz-pattern.md - Network segmentation with DMZ
- references/egress-filtering.md - Outbound traffic control
- references/k8s-networkpolicies.md - Kubernetes pod isolation
Support:
- references/troubleshooting.md - Common issues and solutions
- references/decision-tree.md - Visual guide for tool selection
# Kubernetes NetworkPolicy Examples
#
# These policies implement a default-deny-all approach with explicit allow rules.
# This is recommended for production environments to minimize attack surface.
#
# Prerequisites:
# - CNI plugin that supports NetworkPolicies (Calico, Cilium, Weave)
# - Kubernetes 1.7+
#
# Usage:
# kubectl apply -f default-deny-allow-dns.yaml -n <namespace>
---
# 1. Default Deny All Ingress Traffic
# This policy denies ALL incoming traffic to ALL pods in the namespace.
# You must then create explicit allow policies for required communication.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # Applies to all pods in namespace
policyTypes:
- Ingress
---
# 2. Default Deny All Egress Traffic
# This policy denies ALL outgoing traffic from ALL pods in the namespace.
# You must then create explicit allow policies for required communication.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {} # Applies to all pods in namespace
policyTypes:
- Egress
---
# 3. Allow DNS Queries (Essential)
# Without this, pods cannot resolve domain names.
# This must be applied after default-deny-egress.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {} # Applies to all pods
policyTypes:
- Egress
egress:
# Allow DNS queries to kube-dns/CoreDNS in kube-system namespace
- to:
- namespaceSelector:
matchLabels:
name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# 4. Allow Frontend to Backend Communication
# Frontend pods can send traffic to backend pods on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend # Target: backend pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # Source: frontend pods
ports:
- protocol: TCP
port: 8080
---
# 5. Allow Backend to Database Communication
# Backend pods can send traffic to database pods on port 5432 (PostgreSQL)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-database
namespace: production
spec:
podSelector:
matchLabels:
app: database # Target: database pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend # Source: backend pods
ports:
- protocol: TCP
port: 5432
---
# 6. Allow Ingress Controller to Frontend
# Ingress controller (from ingress-nginx namespace) can access frontend pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: frontend # Target: frontend pods
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
---
# 7. Allow Backend to External API (Egress)
# Backend pods can make HTTPS requests to external APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-external-https
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
# Allow HTTPS to any destination (external APIs)
- to:
- namespaceSelector: {} # Any namespace
ports:
- protocol: TCP
port: 443
# Note: For stricter control, specify exact IP ranges or use DNS-based policies
---
# 8. Allow Prometheus Scraping (Monitoring)
# Prometheus (in monitoring namespace) can scrape metrics from all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scraping
namespace: production
spec:
podSelector: {} # All pods
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090 # Metrics port (adjust as needed)
---
# 9. Allow Same-Namespace Communication
# All pods in the namespace can communicate with each other
# (Useful for development, not recommended for production)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: development
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # From any pod in same namespace
egress:
- to:
- podSelector: {} # To any pod in same namespace
---
# 10. Deny Specific Source (Block Compromised Pod)
# Deny traffic from a specific pod label (e.g., compromised pod)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-compromised-pod
namespace: production
spec:
podSelector: {} # Applies to all pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchExpressions:
- key: compromised
operator: DoesNotExist # Allow only if 'compromised' label is NOT present
#!/usr/sbin/nft -f
# nftables configuration for web server
#
# Features:
# - Default deny with explicit allow
# - Stateful firewall (established/related auto-allowed)
# - SSH from specific IP range only
# - HTTP/HTTPS from anywhere
# - Rate limiting on SSH
# - Logging of dropped packets
#
# Installation:
# 1. Copy to /etc/nftables.conf
# 2. Edit SSH_ALLOWED_IPS set to your office IP range
# 3. Run: sudo nft -f /etc/nftables.conf
# 4. Enable persistence: sudo systemctl enable nftables
flush ruleset
table inet filter {
# Define set of allowed SSH IPs (office/VPN)
set ssh_allowed_ips {
type ipv4_addr
flags interval # Allows CIDR ranges
elements = { 203.0.113.0/24 } # CHANGE THIS to your office IP
}
chain input {
type filter hook input priority 0; policy drop;
# Accept loopback traffic
iif "lo" accept comment "Accept loopback"
# Accept established and related connections (stateful)
ct state established,related accept comment "Accept established connections"
# Drop invalid packets
ct state invalid drop comment "Drop invalid packets"
# Allow ICMP (ping)
meta l4proto icmp accept comment "Accept ICMP ping"
meta l4proto ipv6-icmp accept comment "Accept ICMPv6"
# Allow SSH from allowed IPs only with rate limiting
tcp dport 22 ip saddr @ssh_allowed_ips \
ct state new limit rate 5/minute \
accept comment "SSH from office with rate limit"
# Allow HTTP from anywhere
tcp dport 80 accept comment "Accept HTTP"
# Allow HTTPS from anywhere
tcp dport 443 accept comment "Accept HTTPS"
# Log dropped packets (rate limited to avoid log flooding)
log prefix "nftables-drop: " limit rate 5/minute level info
# Default drop (policy is drop)
}
chain forward {
type filter hook forward priority 0; policy drop;
# No forwarding needed for web server
}
chain output {
type filter hook output priority 0; policy accept;
# Allow all outbound traffic
}
}
# Optional: NAT table for port forwarding (uncomment if needed)
# table inet nat {
# chain prerouting {
# type nat hook prerouting priority -100; policy accept;
# # Example: Forward external 8080 to internal 80
# # tcp dport 8080 dnat to :80
# }
#
# chain postrouting {
# type nat hook postrouting priority 100; policy accept;
# # Example: Masquerade outbound traffic
# # oifname "eth0" masquerade
# }
# }
# Three-Tier Architecture Security Groups
#
# This Terraform configuration creates security groups for a typical
# three-tier web application in AWS:
# - Web tier (public subnet, accessible from Internet)
# - App tier (private subnet, accessible from web tier)
# - Database tier (private subnet, accessible from app tier)
# - Bastion host (public subnet, for SSH access)
#
# Usage:
# 1. Update variables.tf with your VPC ID and CIDR ranges
# 2. terraform init
# 3. terraform plan
# 4. terraform apply
# Variables (create variables.tf)
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "office_cidr" {
description = "Office IP range for SSH access"
type = string
default = "203.0.113.0/24"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
# Bastion Security Group
resource "aws_security_group" "bastion" {
name = "bastion-sg"
description = "Security group for bastion host"
vpc_id = var.vpc_id
# Inbound: SSH from office only
ingress {
description = "SSH from office"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.office_cidr]
}
# Outbound: SSH to VPC instances
egress {
description = "SSH to VPC instances"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
# Outbound: HTTPS for package updates
egress {
description = "HTTPS for updates"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "bastion-sg"
Environment = var.environment
Tier = "bastion"
}
}
# Web Tier Security Group (Public)
resource "aws_security_group" "web" {
name = "web-tier-sg"
description = "Security group for web servers in public subnet"
vpc_id = var.vpc_id
# Inbound: HTTP from Internet
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: HTTPS from Internet
ingress {
description = "HTTPS from Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: SSH from bastion only
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Outbound: To app tier
egress {
description = "HTTP to app tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
# Outbound: HTTPS for external APIs
egress {
description = "HTTPS to Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-tier-sg"
Environment = var.environment
Tier = "web"
}
}
# App Tier Security Group (Private)
resource "aws_security_group" "app" {
name = "app-tier-sg"
description = "Security group for application servers in private subnet"
vpc_id = var.vpc_id
# Inbound: HTTP from web tier
ingress {
description = "HTTP from web tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
# Inbound: SSH from bastion
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Outbound: PostgreSQL to database
egress {
description = "PostgreSQL to database"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.database.id]
}
# Outbound: HTTPS for external APIs
egress {
description = "HTTPS to Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "app-tier-sg"
Environment = var.environment
Tier = "app"
}
}
# Database Tier Security Group (Private)
resource "aws_security_group" "database" {
name = "database-tier-sg"
description = "Security group for RDS database in private subnet"
vpc_id = var.vpc_id
# Inbound: PostgreSQL from app tier only
ingress {
description = "PostgreSQL from app tier"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
# Inbound: SSH from bastion (if using EC2, not RDS)
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Outbound: Minimal (VPC only, no Internet)
egress {
description = "Local VPC only"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [var.vpc_cidr]
}
tags = {
Name = "database-tier-sg"
Environment = var.environment
Tier = "database"
}
}
# Optional: Application Load Balancer Security Group
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = var.vpc_id
# Inbound: HTTPS from Internet
ingress {
description = "HTTPS from Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: HTTP from Internet (redirect to HTTPS)
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: To web tier instances
egress {
description = "HTTP to web tier"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
tags = {
Name = "alb-sg"
Environment = var.environment
Tier = "load-balancer"
}
}
# Outputs
output "bastion_sg_id" {
description = "Bastion security group ID"
value = aws_security_group.bastion.id
}
output "web_sg_id" {
description = "Web tier security group ID"
value = aws_security_group.web.id
}
output "app_sg_id" {
description = "App tier security group ID"
value = aws_security_group.app.id
}
output "database_sg_id" {
description = "Database tier security group ID"
value = aws_security_group.database.id
}
output "alb_sg_id" {
description = "ALB security group ID"
value = aws_security_group.alb.id
}
#!/bin/bash
# Basic Web Server UFW Configuration
#
# This script configures UFW for a typical web server with:
# - SSH access from specific IP range
# - HTTP and HTTPS access from anywhere
# - Rate limiting on SSH to prevent brute force
# - Logging enabled
#
# Usage: sudo ./basic-web-server.sh
set -e # Exit on error
# Configuration
OFFICE_CIDR="203.0.113.0/24" # Change to your office IP range
echo "=== Configuring UFW for Web Server ==="
# Set default policies
echo "Setting default policies..."
ufw default deny incoming
ufw default allow outgoing
# Allow SSH from office only (NOT 0.0.0.0/0 for security)
echo "Allowing SSH from ${OFFICE_CIDR}..."
ufw allow from ${OFFICE_CIDR} to any port 22
# Rate limit SSH to prevent brute force
echo "Enabling SSH rate limiting..."
ufw limit ssh
# Allow HTTP
echo "Allowing HTTP (port 80)..."
ufw allow http
# Allow HTTPS
echo "Allowing HTTPS (port 443)..."
ufw allow https
# Enable logging
echo "Enabling logging..."
ufw logging on
# Show rules before enabling
echo ""
echo "=== Rules to be applied ==="
ufw show added
# Confirm before enabling
read -p "Enable UFW with these rules? (yes/no): " confirm
if [ "$confirm" == "yes" ]; then
echo "Enabling UFW..."
ufw --force enable
echo ""
echo "=== UFW Status ==="
ufw status verbose
echo ""
echo "✅ UFW configuration complete!"
echo ""
echo "Next steps:"
echo "1. Test SSH access from office IP"
echo "2. Test HTTP/HTTPS access from external IP"
echo "3. Run: nmap -Pn <server-ip> (from external) to verify only 22, 80, 443 open"
echo "4. Monitor logs: sudo tail -f /var/log/ufw.log"
else
echo "UFW not enabled. Rules added but not active."
echo "Run 'sudo ufw enable' when ready."
fi
skill: "configuring-firewalls"
version: "1.0"
domain: "security"
base_outputs:
- path: "firewall/"
must_contain:
- "rules"
- "policy"
- path: "docs/firewall-configuration.md"
must_contain:
- "firewall rules"
- "security"
- "ports"
conditional_outputs:
maturity:
starter:
- path: "firewall/ufw-rules.sh"
must_contain: ["ufw", "allow", "enable"]
- path: "firewall/basic-rules.conf"
must_contain: ["default deny", "SSH"]
- path: "docs/firewall-quick-start.md"
must_contain: ["safety checklist", "prevent lockout"]
intermediate:
- path: "firewall/nftables.conf"
must_contain: ["table inet", "chain input", "ct state"]
- path: "firewall/security-groups.tf"
must_contain: ["aws_security_group", "ingress", "egress"]
- path: "firewall/network-policies/"
must_contain: ["NetworkPolicy", "podSelector"]
- path: "scripts/firewall-backup.sh"
must_contain: ["backup", "restore"]
- path: "docs/firewall-patterns.md"
must_contain: ["bastion", "database tier", "web tier"]
advanced:
- path: "firewall/nftables-advanced.conf"
must_contain: ["sets", "maps", "log prefix"]
- path: "terraform/network-security/"
must_contain: ["security_groups", "network_acls", "egress"]
- path: "firewall/egress-filtering.conf"
must_contain: ["outbound", "whitelist", "deny"]
- path: "kubernetes/network-policies/"
must_contain: ["default-deny", "namespace", "egress"]
- path: "scripts/firewall-audit.sh"
must_contain: ["audit", "compliance", "unused rules"]
- path: "docs/dmz-architecture.md"
must_contain: ["DMZ", "network segmentation", "defense-in-depth"]
cloud_provider:
aws:
- path: "terraform/security-groups.tf"
must_contain: ["aws_security_group", "vpc_id"]
- path: "terraform/network-acls.tf"
must_contain: ["aws_network_acl", "subnet_ids"]
- path: "terraform/modules/security-groups/"
must_contain: ["ingress", "egress", "stateful"]
- path: "docs/aws-firewall-guide.md"
must_contain: ["Security Groups", "NACLs", "stateful vs stateless"]
gcp:
- path: "terraform/firewall-rules.tf"
must_contain: ["google_compute_firewall", "priority"]
- path: "terraform/vpc-firewall.tf"
must_contain: ["source_ranges", "target_tags"]
- path: "docs/gcp-firewall-guide.md"
must_contain: ["VPC firewall rules", "priority", "hierarchical"]
azure:
- path: "terraform/nsg.tf"
must_contain: ["azurerm_network_security_group", "security_rule"]
- path: "terraform/nsg-rules.tf"
must_contain: ["priority", "direction", "access"]
- path: "docs/azure-nsg-guide.md"
must_contain: ["Network Security Groups", "NSG", "application security groups"]
multi-cloud:
- path: "terraform/firewall/"
must_contain: ["aws", "gcp", "azure"]
- path: "terraform/modules/"
must_contain: ["security_groups", "firewall_rules", "nsg"]
- path: "docs/multi-cloud-firewall-strategy.md"
must_contain: ["standardization", "cloud-agnostic", "provider-specific"]
infrastructure:
kubernetes:
- path: "k8s/network-policies/default-deny.yaml"
must_contain: ["NetworkPolicy", "podSelector: {}", "Ingress"]
- path: "k8s/network-policies/allow-dns.yaml"
must_contain: ["port: 53", "protocol: UDP"]
- path: "k8s/network-policies/namespace-isolation.yaml"
must_contain: ["namespaceSelector", "podSelector"]
- path: "k8s/network-policies/egress-control.yaml"
must_contain: ["policyTypes", "Egress", "to"]
- path: "docs/k8s-networkpolicy-guide.md"
must_contain: ["CNI", "Calico", "Cilium", "pod-to-pod"]
bare-metal:
- path: "firewall/ufw/"
must_contain: ["ufw", "limit ssh"]
- path: "firewall/nftables/"
must_contain: ["nftables.conf", "inet filter"]
- path: "firewall/iptables/"
must_contain: ["iptables-save", "iptables-restore"]
- path: "scripts/firewall-setup.sh"
must_contain: ["systemctl", "enable"]
- path: "docs/host-based-firewall.md"
must_contain: ["UFW", "nftables", "iptables", "migration"]
containers:
- path: "docker/firewall-rules.sh"
must_contain: ["DOCKER-USER", "iptables"]
- path: "k8s/network-policies/"
must_contain: ["NetworkPolicy", "podSelector"]
- path: "docs/container-firewall.md"
must_contain: ["Docker", "iptables", "NetworkPolicy"]
scaffolding:
- path: "firewall/"
type: "directory"
purpose: "Firewall configuration files (UFW, nftables, iptables)"
- path: "terraform/security/"
type: "directory"
purpose: "Cloud firewall infrastructure as code"
- path: "k8s/network-policies/"
type: "directory"
purpose: "Kubernetes NetworkPolicy manifests"
- path: "scripts/firewall-management/"
type: "directory"
purpose: "Firewall automation and audit scripts"
- path: "firewall/README.md"
type: "file"
purpose: "Overview of firewall configurations and safety procedures"
template: |
# Firewall Configuration
## Overview
Firewall rules for {PROJECT_NAME}. Always test rules before enabling in production.
## Safety Checklist
- [ ] SSH access allowed before enabling firewall
- [ ] Console/physical access available for recovery
- [ ] Rules tested in staging environment
- [ ] Backup of current configuration created
- [ ] Logging enabled for debugging
## Files
- `ufw/` - Ubuntu/Debian firewall rules
- `nftables/` - Modern Linux firewall configuration
- `security-groups.tf` - AWS Security Groups (if applicable)
- `network-policies/` - Kubernetes NetworkPolicies (if applicable)
## Emergency Recovery
If locked out via SSH:
- Cloud: Use console/session manager
- On-prem: Physical console or IPMI/iLO access
- Disable firewall: `sudo ufw disable` or `sudo systemctl stop nftables`
- path: "firewall/.firewall-backup"
type: "directory"
purpose: "Backup directory for firewall rule rollback"
- path: "docs/security/firewall-architecture.md"
type: "file"
purpose: "Documentation of firewall strategy and architecture"
template: |
# Firewall Architecture
## Defense-in-Depth Strategy
### Layer 1: Cloud Network (if applicable)
- Security Groups (AWS) / Firewall Rules (GCP) / NSGs (Azure)
- Network ACLs for subnet-level control
### Layer 2: Host-Based Firewalls
- UFW/nftables on all instances
- Default deny policy
### Layer 3: Application Layer (if applicable)
- Kubernetes NetworkPolicies
- Container network isolation
## Common Patterns
### Web Tier
- Allow: HTTP/HTTPS from 0.0.0.0/0
- Allow: SSH from bastion only
- Deny: All other inbound
### Application Tier
- Allow: App port from web tier only
- Allow: SSH from bastion only
- Deny: All other inbound
### Database Tier
- Allow: Database port from app tier only
- Allow: SSH from bastion only
- Deny: All other inbound, including internet
## Maintenance
- Quarterly audit of firewall rules
- Remove unused rules
- Update documentation when rules change
- Test rule changes in staging first
metadata:
primary_blueprints: ["security"]
contributes_to:
- "Security hardening"
- "Network segmentation"
- "Compliance (PCI-DSS, SOC2, HIPAA)"
- "Defense-in-depth architecture"
common_file_patterns:
host_based:
- "firewall/ufw-rules.sh"
- "firewall/nftables.conf"
- "firewall/iptables-rules.sh"
cloud:
- "terraform/security-groups.tf"
- "terraform/network-acls.tf"
- "terraform/nsg.tf"
- "terraform/firewall-rules.tf"
kubernetes:
- "k8s/network-policies/default-deny.yaml"
- "k8s/network-policies/allow-{service}.yaml"
- "k8s/network-policies/namespace-isolation.yaml"
documentation:
- "docs/firewall-architecture.md"
- "docs/security-groups-guide.md"
- "docs/firewall-troubleshooting.md"
integration_points:
- skill: "security-hardening"
relationship: "Firewalls are one layer of server hardening alongside SSH hardening, fail2ban, and SELinux"
- skill: "infrastructure-as-code"
relationship: "Firewall rules managed as code via Terraform/CloudFormation"
- skill: "kubernetes-operations"
relationship: "NetworkPolicies provide pod-level network isolation in K8s clusters"
- skill: "network-architecture"
relationship: "Firewalls implement network segmentation defined in VPC architecture"
- skill: "monitoring-systems"
relationship: "Firewall logs integrated into centralized logging and alerting"
validation_checks:
- "SSH access rule exists before enabling firewall"
- "Default policy is deny (not accept)"
- "No 0.0.0.0/0 access to sensitive ports (22, 3389, 5432, 3306)"
- "Stateful connection tracking enabled for cloud firewalls"
- "Ephemeral ports (1024-65535) allowed for stateless firewalls (NACLs)"
- "Logging enabled for dropped packets"
- "All firewall configs stored in version control"
AWS Security Groups and Network ACLs Guide
Complete guide to AWS firewall controls with Terraform examples.
Table of Contents
- Security Groups vs Network ACLs
- Security Groups
- Key Characteristics
- Best Practices
- Terraform Examples
- Using Managed Prefix Lists
- Network ACLs
- Key Characteristics
- Best Practices
- Terraform Examples
- Ephemeral Ports Reference
- VPC Flow Logs
- Defense-in-Depth Strategy
- Common Patterns
- Three-Tier Architecture
- Bastion Access
- Load Balancer
- Troubleshooting
- AWS CLI Examples
- Infrastructure as Code Best Practices
Security Groups vs Network ACLs
| Feature | Security Groups | Network ACLs |
|---|---|---|
| Level | Instance (ENI) | Subnet |
| State | Stateful | Stateless |
| Rules | Allow only | Allow + Deny |
| Evaluation | All rules evaluated | Sequential (order matters) |
| Default | Deny inbound, allow outbound | Allow all |
| Return Traffic | Automatic | Must explicitly allow |
| Use Case | Resource-specific control | Subnet-wide policies |
| Rule Limit | 60 inbound + 60 outbound per SG | 20 inbound + 20 outbound per NACL |
Security Groups
Key Characteristics
Stateful Behavior:
- Outbound response traffic automatically allowed
- No need to configure ephemeral ports
- Simpler configuration
Rule Evaluation:
- All rules evaluated (most permissive wins)
- No rule ordering required
- Cannot create explicit deny rules
Scope:
- Attached to Elastic Network Interfaces (ENIs)
- Multiple SGs per instance (up to 5)
- Multiple instances per SG
Best Practices
1. Principle of Least Privilege - Only open required ports 2. Dedicated SGs by Function - Separate web, app, database SGs 3. Reference SGs, Not IPs - Use SG IDs for internal communication 4. Descriptive Names - Use clear, searchable names 5. Description for Every Rule - Document purpose 6. Avoid Default SG - Create custom SGs for active resources 7. Use Managed Prefix Lists - For IP grouping 8. Regular Audits - Quarterly review, remove unused rules 9. Infrastructure as Code - Terraform/CloudFormation 10. Enable VPC Flow Logs - Monitor actual traffic
Terraform Examples
Basic Web Server
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Security group for public web servers"
vpc_id = aws_vpc.main.id
# Inbound rules
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from bastion only"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id] # Reference another SG
}
# Outbound rules
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1" # All protocols
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-server-sg"
Environment = "production"
Tier = "web"
}
}Application Server (Private)
resource "aws_security_group" "app" {
name = "app-server-sg"
description = "Security group for application servers in private subnet"
vpc_id = aws_vpc.main.id
# Allow traffic from web tier
ingress {
description = "HTTP from web tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
# SSH from bastion
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Outbound to database
egress {
description = "PostgreSQL to database"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.database.id]
}
# Outbound HTTPS for API calls
egress {
description = "HTTPS to Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "app-server-sg"
Environment = "production"
Tier = "app"
}
}Database (RDS/Private)
resource "aws_security_group" "database" {
name = "database-sg"
description = "Security group for RDS PostgreSQL database"
vpc_id = aws_vpc.main.id
# Only allow from app tier
ingress {
description = "PostgreSQL from app servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
# SSH from bastion (if using EC2, not RDS)
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id]
}
# Minimal outbound (database typically doesn't need Internet)
egress {
description = "Local VPC only"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.main.cidr_block]
}
tags = {
Name = "database-sg"
Environment = "production"
Tier = "data"
}
}Bastion Host
resource "aws_security_group" "bastion" {
name = "bastion-sg"
description = "Security group for bastion host (jump box)"
vpc_id = aws_vpc.main.id
# Allow SSH from office/VPN only
ingress {
description = "SSH from office"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.office_ip_ranges # ["203.0.113.0/24"]
}
# Outbound SSH to private instances
egress {
description = "SSH to private instances"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
# HTTPS for updates
egress {
description = "HTTPS for package updates"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "bastion-sg"
Environment = "production"
}
}Using Managed Prefix Lists
Managed Prefix Lists allow grouping multiple CIDRs:
resource "aws_ec2_managed_prefix_list" "office_ips" {
name = "office-ip-ranges"
address_family = "IPv4"
max_entries = 10
entry {
cidr = "203.0.113.0/24"
description = "HQ Office"
}
entry {
cidr = "198.51.100.0/24"
description = "Remote Office"
}
tags = {
Name = "office-ips"
}
}
resource "aws_security_group" "example" {
# ... other config
ingress {
description = "SSH from office"
from_port = 22
to_port = 22
protocol = "tcp"
prefix_list_ids = [aws_ec2_managed_prefix_list.office_ips.id]
}
}Network ACLs
Key Characteristics
Stateless Behavior:
- Must explicitly allow return traffic
- Must configure ephemeral ports (1024-65535)
- More complex configuration
Rule Evaluation:
- Sequential (lowest number first)
- First match wins
- Can create explicit deny rules
Scope:
- Applied at subnet level
- One NACL per subnet
- Affects all instances in subnet
Best Practices
1. Use NACLs as Secondary Defense - Primary control via Security Groups 2. Separate NACLs for Public/Private Subnets - Different security postures 3. Number Rules by 100s - Allows easy insertion (100, 200, 300) 4. Place Deny Rules First - Lower numbers = higher priority 5. Remember Ephemeral Ports - 1024-65535 for return traffic 6. Separate IPv4 and IPv6 Rules - Different rule numbers 7. Limit Rule Count - Performance impact with many rules 8. Include ELB Health Checks - Don't block load balancer ranges 9. Document Rule Purpose - Use descriptions in Terraform
Terraform Examples
Public Subnet NACL
resource "aws_network_acl" "public" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.public[*].id
# Inbound rules
# Rule 100: Allow HTTP
ingress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# Rule 110: Allow HTTPS
ingress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# Rule 120: Allow SSH from office
ingress {
rule_no = 120
protocol = "tcp"
action = "allow"
cidr_block = "203.0.113.0/24"
from_port = 22
to_port = 22
}
# Rule 130: CRITICAL - Allow ephemeral ports (return traffic)
ingress {
rule_no = 130
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# Rule 200: Deny specific malicious IP
ingress {
rule_no = 200
protocol = "-1"
action = "deny"
cidr_block = "198.51.100.0/24"
from_port = 0
to_port = 0
}
# Default deny (implicit rule *)
# Outbound rules
# Rule 100: Allow HTTP outbound
egress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# Rule 110: Allow HTTPS outbound
egress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# Rule 120: Allow ephemeral ports (response traffic)
egress {
rule_no = 120
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
tags = {
Name = "public-subnet-nacl"
}
}Private Subnet NACL
resource "aws_network_acl" "private" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.private[*].id
# Inbound from VPC only
# Rule 100: Allow all from VPC
ingress {
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = aws_vpc.main.cidr_block
from_port = 0
to_port = 0
}
# Rule 110: Allow ephemeral ports from Internet (for responses)
ingress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# Outbound
# Rule 100: Allow all to VPC
egress {
rule_no = 100
protocol = "-1"
action = "allow"
cidr_block = aws_vpc.main.cidr_block
from_port = 0
to_port = 0
}
# Rule 110: Allow HTTPS to Internet (for updates)
egress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# Rule 120: Allow DNS
egress {
rule_no = 120
protocol = "udp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 53
to_port = 53
}
# Rule 130: Allow ephemeral ports outbound
egress {
rule_no = 130
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
tags = {
Name = "private-subnet-nacl"
}
}Ephemeral Ports Reference
Different operating systems use different ephemeral port ranges:
- Linux: 32768-60999 (can be narrower: 32768-65535)
- Windows Server 2008+: 49152-65535
- NAT Gateway: 1024-65535
- Application Load Balancer: 1024-65535
Best Practice for NACLs: Allow 1024-65535 to cover all cases.
VPC Flow Logs
Enable Flow Logs to monitor traffic and validate firewall rules:
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL" # ACCEPT, REJECT, or ALL
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.flow_logs.arn
tags = {
Name = "vpc-flow-logs"
}
}
resource "aws_cloudwatch_log_group" "flow_logs" {
name = "/aws/vpc/flow-logs"
retention_in_days = 30
}Query with CloudWatch Insights:
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 100Defense-in-Depth Strategy
Combine Security Groups and NACLs:
Internet
│
├─ Public Subnet NACL (Layer 1: Subnet-wide)
│ └─ Web Server Security Group (Layer 2: Instance-specific)
│ └─ EC2 Instance
│
├─ Private Subnet NACL (Layer 1)
│ └─ App Server Security Group (Layer 2)
│ └─ EC2 Instance
│
└─ Private Subnet NACL (Layer 1)
└─ Database Security Group (Layer 2)
└─ RDS InstanceStrategy:
- Security Groups: Primary control (allow required traffic)
- NACLs: Secondary enforcement (block known threats, enforce subnet policies)
Common Patterns
Three-Tier Architecture
See examples above: Web → App → Database with proper SG/NACL layering.
Bastion Access
1. Bastion SG allows SSH from office IP 2. Private instance SGs allow SSH from bastion SG only 3. No direct Internet access to private instances
Load Balancer
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "To target instances"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
}
# Web server SG allows traffic from ALB
resource "aws_security_group" "web" {
# ...
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
}Troubleshooting
Security Group not blocking traffic:
- Remember: SGs are stateful (return traffic auto-allowed)
- All rules evaluated (can't have conflicting allow/deny)
- Check if multiple SGs attached to instance
NACL blocking expected traffic:
- Check rule order (lowest number first)
- Verify ephemeral ports allowed (1024-65535)
- Confirm both inbound AND outbound rules
- Check for explicit deny rules with lower numbers
Can't SSH to instance:
- Check SG allows SSH from your IP
- Check NACL allows SSH inbound and ephemeral ports outbound
- Verify instance has public IP (or accessible via bastion)
- Check route table and Internet Gateway
VPC Flow Logs showing rejected traffic:
- Filter by action=REJECT
- Check source/dest IP, port, protocol
- Correlate with SG/NACL rules
- Common: Missing ephemeral ports in NACL
AWS CLI Examples
# List security groups
aws ec2 describe-security-groups
# List security group rules
aws ec2 describe-security-group-rules --filters Name=group-id,Values=sg-xxxxx
# Add inbound rule
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxx \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Remove rule
aws ec2 revoke-security-group-ingress \
--group-id sg-xxxxx \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# List NACLs
aws ec2 describe-network-acls
# Describe specific NACL
aws ec2 describe-network-acls --network-acl-ids acl-xxxxxInfrastructure as Code Best Practices
1. Use Terraform Modules - Reusable SG definitions 2. Parameterize CIDRs - Use variables for IP ranges 3. Tag Everything - Name, Environment, Tier, Owner 4. Plan Before Apply - Review changes 5. State Management - Use remote state (S3 + DynamoDB) 6. Separate Environments - Different workspaces/accounts 7. Version Control - Git with meaningful commits 8. Code Review - Security-critical changes reviewed 9. Automated Testing - Validate rules with Terraform tests 10. Documentation - README with architecture diagrams
Azure Network Security Groups (NSGs) Guide
Azure Network Security Groups provide firewall controls for VMs, subnets, and network interfaces.
Table of Contents
- Key Concepts
- NSG Components
- Azure CLI Examples
- Create NSG
- List and View NSGs
- Terraform Examples
- Basic Web Server NSG
- Database NSG (Private)
- Associate NSG with Subnet
- Associate NSG with Network Interface
- Service Tags
- Application Security Groups (ASGs)
- Default Security Rules
- NSG Flow Logs
- Three-Tier Application Example
- Best Practices
- Comparison with AWS/GCP
- Resources
Key Concepts
NSG Characteristics:
- Stateful: Return traffic automatically allowed
- Priority-based: 100-4096 (lower = higher priority)
- Subnet or NIC level: Can attach to subnet or network interface
- Default rules: Cannot be deleted, lowest priority
NSG Components
Priority: 100-4096 (lower = higher priority)
Name: Descriptive rule name
Port: Port or port range
Protocol: TCP, UDP, ICMP, or Any
Source: IP, Service Tag, or Application Security Group
Destination: IP, Service Tag, or Application Security Group
Action: Allow or Deny
Direction: Inbound or OutboundAzure CLI Examples
Create NSG
# Create NSG
az network nsg create \
--resource-group myResourceGroup \
--name web-nsg \
--location eastus
# Create rule
az network nsg rule create \
--resource-group myResourceGroup \
--nsg-name web-nsg \
--name allow-http \
--priority 100 \
--source-address-prefixes '*' \
--source-port-ranges '*' \
--destination-address-prefixes '*' \
--destination-port-ranges 80 \
--access Allow \
--protocol Tcp \
--direction InboundList and View NSGs
# List NSGs
az network nsg list --output table
# Show NSG details
az network nsg show --resource-group myResourceGroup --name web-nsg
# List rules
az network nsg rule list --resource-group myResourceGroup --nsg-name web-nsg --output tableTerraform Examples
Basic Web Server NSG
resource "azurerm_network_security_group" "web" {
name = "web-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
# Inbound: Allow HTTP
security_rule {
name = "allow-http"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Inbound: Allow HTTPS
security_rule {
name = "allow-https"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Inbound: Allow SSH from office
security_rule {
name = "allow-ssh-office"
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "203.0.113.0/24" # Office IP
destination_address_prefix = "*"
}
tags = {
Environment = "Production"
Tier = "Web"
}
}Database NSG (Private)
resource "azurerm_network_security_group" "database" {
name = "database-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
# Inbound: Allow PostgreSQL from app subnet
security_rule {
name = "allow-postgresql-from-app"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "5432"
source_address_prefix = "10.0.2.0/24" # App subnet
destination_address_prefix = "*"
}
# Deny all other inbound
security_rule {
name = "deny-all-inbound"
priority = 4000
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = {
Environment = "Production"
Tier = "Database"
}
}Associate NSG with Subnet
resource "azurerm_subnet_network_security_group_association" "web" {
subnet_id = azurerm_subnet.web.id
network_security_group_id = azurerm_network_security_group.web.id
}Associate NSG with Network Interface
resource "azurerm_network_interface_security_group_association" "web_vm" {
network_interface_id = azurerm_network_interface.web_vm.id
network_security_group_id = azurerm_network_security_group.web.id
}Service Tags
Azure service tags represent groups of IP addresses:
resource "azurerm_network_security_group" "app" {
name = "app-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
# Allow to Azure Storage
security_rule {
name = "allow-storage"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "Storage" # Service tag
}
# Allow to Azure SQL
security_rule {
name = "allow-sql"
priority = 110
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "1433"
source_address_prefix = "*"
destination_address_prefix = "Sql" # Service tag
}
}Common Service Tags:
Internet- All Internet addressesVirtualNetwork- All VNet addressesAzureLoadBalancer- Azure load balancerStorage- Azure StorageSql- Azure SQL DatabaseAzureMonitor- Azure Monitor
Application Security Groups (ASGs)
Group VMs logically without IP addresses:
# Define ASGs
resource "azurerm_application_security_group" "web" {
name = "web-asg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_application_security_group" "app" {
name = "app-asg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
# Use ASGs in NSG rules
resource "azurerm_network_security_group" "main" {
name = "main-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "web-to-app"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_application_security_group_ids = [azurerm_application_security_group.web.id]
destination_application_security_group_ids = [azurerm_application_security_group.app.id]
}
}
# Associate VM NIC with ASG
resource "azurerm_network_interface_application_security_group_association" "web_vm" {
network_interface_id = azurerm_network_interface.web_vm.id
application_security_group_id = azurerm_application_security_group.web.id
}Default Security Rules
Azure creates default rules (cannot be deleted):
Inbound:
- Priority 65000: Allow VNet → VNet
- Priority 65001: Allow AzureLoadBalancer → Any
- Priority 65500: Deny All
Outbound:
- Priority 65000: Allow VNet → VNet
- Priority 65001: Allow Any → Internet
- Priority 65500: Deny All
Override with custom rules (priority 100-4096).
NSG Flow Logs
Enable for monitoring:
resource "azurerm_network_watcher_flow_log" "nsg" {
network_watcher_name = azurerm_network_watcher.main.name
resource_group_name = azurerm_resource_group.main.name
name = "nsg-flow-log"
network_security_group_id = azurerm_network_security_group.web.id
storage_account_id = azurerm_storage_account.logs.id
enabled = true
retention_policy {
enabled = true
days = 30
}
traffic_analytics {
enabled = true
workspace_id = azurerm_log_analytics_workspace.main.workspace_id
workspace_region = azurerm_log_analytics_workspace.main.location
workspace_resource_id = azurerm_log_analytics_workspace.main.id
interval_in_minutes = 10
}
}Three-Tier Application Example
# Web Tier NSG
resource "azurerm_network_security_group" "web" {
name = "web-tier-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "allow-https"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
security_rule {
name = "allow-to-app"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_application_security_group_ids = [azurerm_application_security_group.web.id]
destination_application_security_group_ids = [azurerm_application_security_group.app.id]
}
}
# App Tier NSG
resource "azurerm_network_security_group" "app" {
name = "app-tier-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "allow-from-web"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8080"
source_application_security_group_ids = [azurerm_application_security_group.web.id]
destination_application_security_group_ids = [azurerm_application_security_group.app.id]
}
security_rule {
name = "allow-to-database"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "5432"
source_address_prefix = "*"
destination_address_prefix = "10.0.3.0/24" # Database subnet
}
}
# Database Tier NSG
resource "azurerm_network_security_group" "database" {
name = "database-tier-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "allow-from-app"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "5432"
source_address_prefix = "10.0.2.0/24" # App subnet
destination_address_prefix = "*"
}
security_rule {
name = "deny-outbound-internet"
priority = 4000
direction = "Outbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "Internet"
}
}Best Practices
1. Use Application Security Groups: Logical grouping, not IP-based 2. Descriptive Names: Clear rule names and descriptions 3. Priority Spacing: Use 100, 110, 120 for easy insertion 4. Service Tags: Use tags instead of hardcoded IPs 5. Least Privilege: Only open necessary ports 6. Flow Logs: Enable for monitoring and troubleshooting 7. Infrastructure as Code: Manage with Terraform 8. Regular Audits: Review rules quarterly 9. Document Rules: Add descriptions to every rule 10. Defense-in-Depth: NSGs on subnet AND NIC
Comparison with AWS/GCP
| Feature | Azure NSG | AWS Security Groups | GCP Firewall Rules |
|---|---|---|---|
| Level | Subnet or NIC | Instance (ENI) | VPC |
| Priority | Yes (100-4096) | No | Yes (0-65535) |
| Deny Rules | Yes | No | Yes |
| Stateful | Yes | Yes | Yes |
| Service Tags | Yes | No (Prefix Lists) | Yes |
| ASGs | Yes | No | No (Tags) |
Resources
- Azure NSG Documentation: https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview
- Terraform azurerm_network_security_group: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group
Bastion Host Pattern
A bastion host (jump box) is a hardened server that provides secure access to private instances in a network.
Table of Contents
- Architecture
- UFW Configuration (Bastion Host)
- UFW Configuration (Private Instances)
- AWS Security Groups
- Bastion Security Group
- Private Instance Security Group
- Connecting Through Bastion
- Manual SSH Jump
- SSH ProxyJump (Recommended)
- SSH Agent Forwarding (Not Recommended)
- SCP Through Bastion
- Hardening the Bastion Host
- OS-Level Hardening
- SSH Configuration (/etc/ssh/sshd_config)
- Monitoring and Auditing
- CloudWatch Alarms (AWS)
- Terraform Complete Example
- Alternative: AWS Systems Manager Session Manager
- Best Practices Summary
- Troubleshooting
Architecture
Internet
│
└─ Public Subnet
└─ Bastion Host (Single Entry Point)
│
└─ Private Subnet(s)
├─ App Server 1
├─ App Server 2
└─ Database ServerKey Principles:
- Single hardened entry point for administrative access
- Bastion in public subnet with public IP
- Private instances in private subnets (no public IPs)
- SSH from private instances only via bastion
- Heavily monitored and audited
UFW Configuration (Bastion Host)
# On bastion host
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH from office/VPN only (NOT 0.0.0.0/0)
sudo ufw allow from 203.0.113.0/24 to any port 22
# Or specific IPs
sudo ufw allow from 203.0.113.10 to any port 22
sudo ufw allow from 203.0.113.20 to any port 22
# Rate limit SSH (prevent brute force)
sudo ufw limit ssh
# Enable logging
sudo ufw logging on
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status verboseUFW Configuration (Private Instances)
# On private app/database servers
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH from bastion only (use bastion's private IP)
sudo ufw allow from 10.0.1.5 to any port 22
# Allow application-specific ports from appropriate sources
# Example for web server:
sudo ufw allow from 10.0.0.0/16 to any port 80 # Internal traffic only
# Enable
sudo ufw enableAWS Security Groups
Bastion Security Group
resource "aws_security_group" "bastion" {
name = "bastion-sg"
description = "Security group for bastion host"
vpc_id = aws_vpc.main.id
# Inbound: SSH from office/VPN only
ingress {
description = "SSH from office"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.office_ip_ranges # ["203.0.113.0/24"]
}
# Outbound: SSH to private instances
egress {
description = "SSH to VPC instances"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
# Outbound: HTTPS for updates
egress {
description = "HTTPS for package updates"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "bastion-sg"
Environment = var.environment
}
}Private Instance Security Group
resource "aws_security_group" "private_instance" {
name = "private-instance-sg"
description = "Security group for private instances (SSH via bastion)"
vpc_id = aws_vpc.main.id
# Inbound: SSH from bastion only
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion.id] # Reference bastion SG
}
# Inbound: Application-specific ports
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
# Outbound: All (or restrict as needed)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "private-instance-sg"
Environment = var.environment
}
}Connecting Through Bastion
Manual SSH Jump
# Two-step SSH
ssh -i bastion-key.pem ec2-user@bastion-public-ip
# Then from bastion:
ssh -i private-key.pem ec2-user@private-instance-ipSSH ProxyJump (Recommended)
# Single command
ssh -J ec2-user@bastion-public-ip ec2-user@private-instance-ip
# Or configure in ~/.ssh/config
Host bastion
HostName bastion-public-ip
User ec2-user
IdentityFile ~/.ssh/bastion-key.pem
Host private-*
User ec2-user
IdentityFile ~/.ssh/private-key.pem
ProxyJump bastion
# Then simply:
ssh private-app-server-ipSSH Agent Forwarding (Not Recommended)
# Forward SSH agent (security risk - avoid if possible)
ssh -A ec2-user@bastion-public-ipWhy Not Recommended:
- Forwarded agent can be hijacked if bastion compromised
- Use ProxyJump instead
SCP Through Bastion
# Copy file to private instance via bastion
scp -o ProxyJump=ec2-user@bastion-public-ip \
local-file.txt ec2-user@private-instance-ip:/home/ec2-user/
# Copy from private instance
scp -o ProxyJump=ec2-user@bastion-public-ip \
ec2-user@private-instance-ip:/var/log/app.log ./Hardening the Bastion Host
OS-Level Hardening
# Update packages
sudo apt update && sudo apt upgrade -y
# Install fail2ban (block brute force)
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Disable root login
sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
# Disable password authentication (key-only)
sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
# Restart SSH
sudo systemctl restart sshd
# Enable automatic security updates
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgradesSSH Configuration (/etc/ssh/sshd_config)
# Best practices for bastion
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
# Limit SSH users (optional)
AllowUsers ec2-user admin
# Use strong ciphers only
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256Monitoring and Auditing
# Enable auditd
sudo apt install auditd
sudo systemctl enable auditd
sudo systemctl start auditd
# Monitor SSH logins
sudo tail -f /var/log/auth.log
# CloudWatch Logs (AWS)
# Install CloudWatch agent and configure to ship:
# - /var/log/auth.log (SSH attempts)
# - /var/log/fail2ban.log (blocked IPs)
# - /var/log/ufw.log (firewall logs)CloudWatch Alarms (AWS)
resource "aws_cloudwatch_log_metric_filter" "bastion_ssh_failed" {
name = "bastion-ssh-failed-attempts"
log_group_name = aws_cloudwatch_log_group.bastion.name
pattern = "[Mon, day, timestamp, ip, id, msg1 = Failed, msg2 = password, ...]"
metric_transformation {
name = "SSHFailedLoginAttempts"
namespace = "Bastion"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "bastion_ssh_failed" {
alarm_name = "bastion-ssh-failed-attempts"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "SSHFailedLoginAttempts"
namespace = "Bastion"
period = "300" # 5 minutes
statistic = "Sum"
threshold = "5"
alarm_description = "Alert on multiple failed SSH attempts"
alarm_actions = [aws_sns_topic.alerts.arn]
}Terraform Complete Example
# Bastion host EC2 instance
resource "aws_instance" "bastion" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.bastion.id]
associate_public_ip_address = true
key_name = aws_key_pair.bastion.key_name
user_data = <<-EOF
#!/bin/bash
apt update
apt upgrade -y
apt install -y fail2ban ufw
# Configure UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow from ${var.office_cidr} to any port 22
ufw limit ssh
ufw enable
# Configure fail2ban
systemctl enable fail2ban
systemctl start fail2ban
EOF
tags = {
Name = "bastion-host"
Environment = var.environment
Role = "bastion"
}
monitoring = true # Enable detailed CloudWatch monitoring
}
# Elastic IP for bastion (static IP)
resource "aws_eip" "bastion" {
instance = aws_instance.bastion.id
domain = "vpc"
tags = {
Name = "bastion-eip"
}
}Alternative: AWS Systems Manager Session Manager
Instead of bastion host, use AWS SSM Session Manager:
Advantages:
- No public IP needed on bastion
- No SSH keys to manage
- Built-in audit logging
- Port forwarding support
- No inbound firewall rules
Setup:
# IAM role for EC2 instances
resource "aws_iam_role" "ssm" {
name = "ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Attach to instances
resource "aws_iam_instance_profile" "ssm" {
name = "ec2-ssm-profile"
role = aws_iam_role.ssm.name
}
resource "aws_instance" "private" {
# ... other config
iam_instance_profile = aws_iam_instance_profile.ssm.name
}Connect:
# Install AWS CLI and Session Manager plugin
# https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
# Connect to instance
aws ssm start-session --target i-1234567890abcdef0
# Port forwarding
aws ssm start-session --target i-1234567890abcdef0 \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["22"],"localPortNumber":["9999"]}'
# Then SSH via localhost:9999
ssh -i private-key.pem ec2-user@localhost -p 9999Best Practices Summary
1. Limit Access - Bastion accessible from office/VPN only (never 0.0.0.0/0) 2. Key-Based Auth Only - Disable password authentication 3. Rate Limiting - Use UFW limit or fail2ban 4. Monitoring - CloudWatch Logs + Alarms for failed attempts 5. Hardening - Disable root, strong SSH ciphers, automatic updates 6. Minimal Bastion - No application code, minimal packages 7. Audit Logging - Ship all logs to centralized logging (CloudWatch, Splunk) 8. Regular Updates - Automated security patches 9. Use Session Manager - Consider SSM Session Manager for bastion-less access 10. Multiple Availability Zones - Deploy bastion in multiple AZs for HA
Troubleshooting
Can't SSH to bastion:
- Check Security Group allows SSH from your IP
- Verify bastion has public IP
- Check NACL allows SSH inbound + ephemeral ports outbound
- Verify SSH key is correct
Can't SSH from bastion to private instance:
- Check private instance SG allows SSH from bastion SG
- Verify bastion can reach private subnet (route table)
- Ensure SSH key for private instance is available
- Check private instance NACL allows SSH
Locked out of bastion:
- Use EC2 Instance Connect (if enabled)
- Use AWS Systems Manager Session Manager
- Last resort: Stop instance, detach root volume, mount to rescue instance, fix SSH config
Session Manager not working:
- Verify IAM role attached with AmazonSSMManagedInstanceCore
- Check instance has outbound HTTPS to SSM endpoints
- Ensure SSM agent installed and running:
sudo systemctl status amazon-ssm-agent - Check VPC endpoints configured (if private subnet with no NAT)
Database Firewall Patterns
Firewall configurations for database servers with strict access control.
Table of Contents
- Core Principles
- UFW Configuration (PostgreSQL)
- nftables Configuration (PostgreSQL)
- AWS Security Group (RDS PostgreSQL)
- Database Types and Ports
- MySQL Example (nftables)
- MongoDB Example (UFW)
- Connection Pooling Considerations
- Read Replicas
- Database Monitoring Access
- Best Practices
- PostgreSQL SSL/TLS Enforcement
- Testing Database Firewall
- Troubleshooting
- Resources
Core Principles
1. Deny All by Default: Database should not be accessible from Internet 2. Application Tier Only: Allow connections only from app servers 3. SSH from Bastion: Administrative access via bastion host 4. No Direct Internet: Database should not initiate outbound Internet connections 5. Monitoring: Enhanced logging for database access
UFW Configuration (PostgreSQL)
# Database server (10.0.3.10)
# App servers: 10.0.2.10, 10.0.2.11, 10.0.2.12
# Bastion: 10.0.1.5
# Set defaults
sudo ufw default deny incoming
sudo ufw default deny outgoing # Restrict outbound too
# Allow SSH from bastion only
sudo ufw allow from 10.0.1.5 to any port 22
# Allow PostgreSQL from app servers only
sudo ufw allow from 10.0.2.10 to any port 5432
sudo ufw allow from 10.0.2.11 to any port 5432
sudo ufw allow from 10.0.2.12 to any port 5432
# Or allow from entire app subnet
sudo ufw allow from 10.0.2.0/24 to any port 5432
# Allow outbound DNS (for name resolution)
sudo ufw allow out 53
# Allow outbound to VPC only (for responses)
sudo ufw allow out to 10.0.0.0/8
# Enable logging
sudo ufw logging on
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status verbosenftables Configuration (PostgreSQL)
#!/usr/sbin/nft -f
# Database server firewall
flush ruleset
table inet filter {
# App tier IPs
set app_servers {
type ipv4_addr
elements = { 10.0.2.10, 10.0.2.11, 10.0.2.12 }
}
# Bastion IP
set bastion {
type ipv4_addr
elements = { 10.0.1.5 }
}
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
# SSH from bastion only
tcp dport 22 ip saddr @bastion accept
# PostgreSQL from app servers only
tcp dport 5432 ip saddr @app_servers ct state new limit rate 100/second accept
# Log dropped connections
log prefix "db-drop: " limit rate 5/minute level warn
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# DNS
udp dport 53 accept
tcp dport 53 accept
# Internal network only (no Internet)
ip daddr 10.0.0.0/8 accept
# Log blocked egress
log prefix "db-egress-block: " limit rate 2/minute level warn
}
}AWS Security Group (RDS PostgreSQL)
resource "aws_security_group" "database" {
name = "rds-postgresql-sg"
description = "Security group for RDS PostgreSQL database"
vpc_id = aws_vpc.main.id
# Inbound: PostgreSQL from app tier only
ingress {
description = "PostgreSQL from app servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id] # Reference app SG
}
# Outbound: Minimal (local VPC only)
egress {
description = "Local VPC only"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.main.cidr_block]
}
tags = {
Name = "rds-postgresql-sg"
Environment = var.environment
Tier = "database"
}
}
# RDS instance with security group
resource "aws_db_instance" "postgres" {
identifier = "myapp-postgres"
engine = "postgres"
engine_version = "15.3"
instance_class = "db.t3.medium"
allocated_storage = 100
db_name = "myapp"
username = "dbadmin"
password = var.db_password
vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.database.name
# Network isolation
publicly_accessible = false
# Encryption
storage_encrypted = true
# Backups
backup_retention_period = 7
# Monitoring
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
tags = {
Name = "myapp-postgres"
}
}Database Types and Ports
| Database | Default Port | Protocol |
|---|---|---|
| PostgreSQL | 5432 | TCP |
| MySQL/MariaDB | 3306 | TCP |
| MongoDB | 27017 | TCP |
| Redis | 6379 | TCP |
| Cassandra | 9042 | TCP |
| Elasticsearch | 9200 | TCP |
| SQL Server | 1433 | TCP |
| Oracle | 1521 | TCP |
MySQL Example (nftables)
table inet filter {
set app_servers {
type ipv4_addr
elements = { 10.0.2.10, 10.0.2.11 }
}
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
# MySQL from app servers
tcp dport 3306 ip saddr @app_servers accept
# SSH from bastion
tcp dport 22 ip saddr 10.0.1.5 accept
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# DNS
udp dport 53 accept
# Internal only
ip daddr 10.0.0.0/8 accept
}
}MongoDB Example (UFW)
# MongoDB default port 27017
sudo ufw default deny incoming
sudo ufw default deny outgoing
# Allow from app servers
sudo ufw allow from 10.0.2.0/24 to any port 27017
# SSH from bastion
sudo ufw allow from 10.0.1.5 to any port 22
# Outbound DNS
sudo ufw allow out 53
# Outbound internal
sudo ufw allow out to 10.0.0.0/8
sudo ufw enableConnection Pooling Considerations
With connection poolers (PgBouncer, ProxySQL):
# Allow app servers to pooler
sudo ufw allow from 10.0.2.0/24 to any port 6432 # PgBouncer
# Allow pooler to database (localhost if on same host)
# Or if separate:
sudo ufw allow from <pooler-ip> to any port 5432Read Replicas
For database replication:
# On primary database
sudo ufw allow from <replica-ip> to any port 5432
# PostgreSQL replication (if using different port)
sudo ufw allow from <replica-ip> to any port 5433Database Monitoring Access
Allow monitoring tools:
# Prometheus postgres_exporter
sudo ufw allow from <monitoring-server> to any port 9187
# Or use localhost-only exporter
# postgres_exporter listens on 127.0.0.1:9187
# SSH tunnel from monitoring serverBest Practices
1. Never Expose to Internet: Database should always be in private subnet 2. Application Security Group Reference: Use SG IDs, not IPs 3. Egress Restrictions: Prevent data exfiltration via outbound blocks 4. Rate Limiting: Limit connection attempts (nftables) 5. Logging: Enable comprehensive connection logging 6. Encryption in Transit: Require SSL/TLS connections 7. No Root from Network: Disable network root login (MySQL) 8. VPC Peering: Use VPC peering for cross-VPC database access 9. PrivateLink: Use AWS PrivateLink for secure cross-account access 10. Backup Access: Ensure backup tools can reach database
PostgreSQL SSL/TLS Enforcement
In /etc/postgresql/*/main/postgresql.conf:
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'In /etc/postgresql/*/main/pg_hba.conf:
# Require SSL for all remote connections
hostssl all all 10.0.2.0/24 md5Testing Database Firewall
# From app server (should succeed)
psql -h 10.0.3.10 -U myapp -d mydb
# From bastion (should fail - database connection)
psql -h 10.0.3.10 -U myapp -d mydb
# From bastion (should succeed - SSH)
ssh admin@10.0.3.10
# From Internet (should timeout)
telnet <public-ip> 5432 # Should not be reachableTroubleshooting
Can't connect from app server:
- Check firewall allows app server IP:
sudo ufw status - Verify database listening:
ss -tuln | grep 5432 - Check PostgreSQL config:
listen_addresses = '*'in postgresql.conf - Check pg_hba.conf allows app subnet
Locked out of database server:
- Use bastion to SSH:
ssh -J bastion admin@database - Check UFW status:
sudo ufw status - If needed, disable temporarily via console access
Replication not working:
- Ensure replica IP allowed on primary database
- Check firewall on both primary and replica
- Verify replication port (often same as main port)
Resources
- PostgreSQL Security: https://www.postgresql.org/docs/current/auth-pg-hba-conf.html
- MySQL Security: https://dev.mysql.com/doc/refman/8.0/en/security.html
- For AWS RDS Security Groups configuration, see the main SKILL.md
- For nftables and UFW patterns, see the main SKILL.md
Firewall Decision Tree
Visual guide for choosing the right firewall tool for your situation.
Table of Contents
- Quick Decision Matrix
- Detailed Decision Flow
- Feature Comparison
- Use Case Recommendations
- Simple Web Server
- High-Performance Server
- Database Server
- Enterprise Multi-Server
- Kubernetes Cluster
- Legacy System
- When to Use Multiple Layers
- Migration Path
- Quick Reference Commands
- Check What's Running
- Decision Factors Summary
Quick Decision Matrix
| Context | Recommended Tool | Alternative |
|---|---|---|
| Ubuntu/Debian server | UFW | nftables |
| RHEL/CentOS/Fedora | firewalld | nftables |
| Modern Linux (kernel 4.14+) | nftables | UFW/firewalld |
| Legacy Linux (kernel < 4.14) | iptables | - |
| AWS EC2 instances | Security Groups | + host firewall (UFW/nftables) |
| GCP Compute Engine | VPC Firewall Rules | + host firewall |
| Azure VMs | Network Security Groups | + host firewall |
| Kubernetes pods | NetworkPolicies | - |
| High performance needs | nftables | iptables |
Detailed Decision Flow
START: Need to configure firewall
│
├─── Running in Cloud?
│ │
│ ├─ YES → Which cloud provider?
│ │ ├─ AWS → Use Security Groups (primary)
│ │ │ + NACLs (secondary, subnet-level)
│ │ │ + Host firewall (defense-in-depth)
│ │ │
│ │ ├─ GCP → Use VPC Firewall Rules
│ │ │ + Host firewall (defense-in-depth)
│ │ │
│ │ └─ Azure → Use Network Security Groups
│ │ + Host firewall (defense-in-depth)
│ │
│ └─ NO → Continue to host-based firewalls
│
├─── Operating System?
│ │
│ ├─ Ubuntu/Debian
│ │ ├─ Simple requirements → UFW ✓ (recommended)
│ │ ├─ Advanced control → nftables
│ │ └─ Legacy system → iptables
│ │
│ ├─ RHEL/CentOS/Fedora
│ │ ├─ Standard setup → firewalld ✓ (default)
│ │ ├─ Advanced control → nftables
│ │ └─ Legacy (RHEL 6) → iptables
│ │
│ ├─ Modern Linux (kernel 4.14+)
│ │ ├─ Performance critical → nftables ✓
│ │ ├─ Simplicity preferred → UFW
│ │ └─ Existing scripts → iptables (migrate later)
│ │
│ └─ Old Linux (kernel < 4.14)
│ └─ Only option → iptables
│
├─── Kubernetes Environment?
│ │
│ ├─ YES → Check CNI plugin
│ │ ├─ Calico, Cilium, Weave → NetworkPolicies ✓
│ │ └─ Flannel → No NetworkPolicy support (upgrade CNI)
│ │
│ └─ NO → Continue to other considerations
│
├─── Performance Requirements?
│ │
│ ├─ High throughput (>100k pps) → nftables (O(log n))
│ ├─ Standard workload → Any tool acceptable
│ └─ Low resource system → UFW or nftables
│
├─── Stateful or Stateless?
│ │
│ ├─ Stateful (recommended for most)
│ │ → Security Groups, UFW, nftables default, iptables with conntrack
│ │
│ └─ Stateless (specialized needs)
│ → Network ACLs, custom nftables/iptables rules
│
└─── Multiple Layers Needed?
│
├─ YES → Defense-in-Depth
│ ├─ Cloud: Security Groups + NACLs
│ ├─ Host: UFW/nftables + fail2ban
│ └─ Container: NetworkPolicies
│
└─ NO → Choose most appropriate single layerFeature Comparison
| Feature | UFW | nftables | iptables | firewalld | Security Groups | NetworkPolicies |
|---|---|---|---|---|---|---|
| Ease of Use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Performance | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Flexibility | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| IPv6 Support | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stateful | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| NAT Support | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Dynamic Updates | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
| GUI Available | ❌ | ❌ | ❌ | ✅ | ✅ (Console) | ❌ |
Use Case Recommendations
Simple Web Server
Recommendation: UFW
- Easy to configure
- Sufficient for most web servers
- Good defaults
High-Performance Server
Recommendation: nftables
- O(log n) performance
- Efficient rule processing
- Modern kernel features
Database Server
Recommendation: UFW or nftables
- UFW: Simple IP/port restrictions
- nftables: Advanced egress filtering
Enterprise Multi-Server
Recommendation: Infrastructure as Code (Terraform) + host firewall
- Cloud: Security Groups/NSGs via Terraform
- Host: Ansible-managed UFW/nftables
Kubernetes Cluster
Recommendation: NetworkPolicies + node firewalls
- NetworkPolicies: Pod-to-pod control
- UFW/nftables on nodes: External access control
Legacy System
Recommendation: iptables
- Stick with existing tool
- Plan migration to nftables
When to Use Multiple Layers
Defense-in-Depth Strategy:
┌─────────────────────────────────────┐
│ Cloud Network ACLs (Layer 1) │ ← Subnet-wide policies
│ ├─ Deny known malicious IPs │
│ └─ Enforce ephemeral port policies │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Cloud Security Groups (Layer 2) │ ← Instance-specific
│ ├─ Allow required services │
│ └─ Reference-based rules │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Host Firewall (Layer 3) │ ← Fine-grained control
│ ├─ UFW/nftables rules │
│ ├─ fail2ban integration │
│ └─ Application-specific ports │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Container NetworkPolicies (Layer 4)│ ← Pod isolation
│ ├─ Default deny │
│ └─ Explicit allow rules │
└─────────────────────────────────────┘Use Multiple Layers When:
- High security requirements (PCI-DSS, HIPAA)
- Public-facing services
- Multi-tenant environments
- Zero-trust architecture
Single Layer Sufficient When:
- Internal development environments
- Trusted network zones
- Resource-constrained systems
Migration Path
Current State → Recommended State:
iptables (legacy)
└─→ nftables (modern, better performance)
No firewall
└─→ UFW (quick hardening) or nftables (production)
Cloud only (Security Groups)
└─→ + Host firewall (defense-in-depth)
No NetworkPolicies (K8s)
└─→ NetworkPolicies (pod isolation)Quick Reference Commands
Check What's Running
# UFW
sudo ufw status
# nftables
sudo nft list ruleset
sudo systemctl status nftables
# iptables
sudo iptables -L -v -n
sudo systemctl status iptables
# firewalld
sudo firewall-cmd --state
sudo systemctl status firewalld
# AWS
aws ec2 describe-security-groups
# Kubernetes
kubectl get networkpolicies -ADecision Factors Summary
Choose UFW if:
- Ubuntu/Debian server
- Simple requirements
- Quick setup needed
- Prefer simplicity over advanced features
Choose nftables if:
- Modern Linux (kernel 4.14+)
- Need high performance
- Complex rule requirements
- Want unified IPv4/IPv6/NAT syntax
Choose firewalld if:
- RHEL/CentOS/Fedora
- Zone-based management needed
- Dynamic updates required
- GUI management desired
Choose iptables if:
- Legacy system (kernel < 4.14)
- Existing automation
- Migration not yet feasible
Use Cloud Firewalls if:
- Running in AWS/GCP/Azure
- Need centralized management
- Infrastructure as Code approach
- Multi-account/multi-VPC setup
Use NetworkPolicies if:
- Kubernetes environment
- CNI plugin supports it
- Need pod-level isolation
- Implementing zero-trust
DMZ (Demilitarized Zone) Pattern
DMZ architecture isolates public-facing services from internal networks, providing an additional security layer.
Table of Contents
- Architecture Overview
- Key Principles
- AWS Implementation
- VPC and Subnet Setup
- DMZ Security Group
- DMZ Network ACL
- On-Premise DMZ (nftables)
- DMZ Firewall
- Monitoring DMZ Traffic
- VPC Flow Logs
- CloudWatch Alarms
- Best Practices
- Common DMZ Mistakes
- DMZ vs Bastion Host
- Testing DMZ Configuration
- Resources
Architecture Overview
Internet
│
▼
┌───────────────────────────┐
│ Public Subnet (DMZ) │ ← Web/API servers, Load Balancers
│ - Accepts Internet traffic│
│ - Limited outbound access │
│ - Heavily monitored │
└───────────┬───────────────┘
│
▼ (One-way: DMZ → App)
┌───────────────────────────┐
│ Private Subnet (App Tier) │ ← Application servers
│ - No direct Internet │
│ - Accepts from DMZ only │
└───────────┬───────────────┘
│
▼ (One-way: App → Data)
┌───────────────────────────┐
│ Private Subnet (Data Tier)│ ← Databases
│ - No Internet access │
│ - Accepts from App only │
└───────────────────────────┘Key Principles
1. Network Segmentation: Separate subnets for each tier 2. Controlled Connectivity: DMZ can't directly access database 3. Minimal Exposure: Only necessary ports open externally 4. Layered Firewall Rules: NACLs + Security Groups (AWS) or equivalent 5. Monitoring: Extra logging and alerting for DMZ traffic
AWS Implementation
VPC and Subnet Setup
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "dmz-vpc"
}
}
# DMZ Subnet (Public)
resource "aws_subnet" "dmz" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "us-east-1a"
tags = {
Name = "dmz-subnet"
Tier = "dmz"
}
}
# App Subnet (Private)
resource "aws_subnet" "app" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "app-subnet"
Tier = "app"
}
}
# Data Subnet (Private)
resource "aws_subnet" "data" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
availability_zone = "us-east-1a"
tags = {
Name = "data-subnet"
Tier = "data"
}
}
# Internet Gateway for DMZ
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "dmz-igw"
}
}
# NAT Gateway for private subnets (optional)
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.dmz.id
tags = {
Name = "dmz-nat-gw"
}
}DMZ Security Group
resource "aws_security_group" "dmz_web" {
name = "dmz-web-sg"
description = "DMZ web servers (public-facing)"
vpc_id = aws_vpc.main.id
# Inbound: HTTPS from Internet
ingress {
description = "HTTPS from Internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Inbound: HTTP (redirect to HTTPS)
ingress {
description = "HTTP from Internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: To app tier only (no general Internet)
egress {
description = "To app tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["10.0.2.0/24"] # App subnet
}
# Outbound: DNS
egress {
description = "DNS"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
# Outbound: HTTPS for external APIs (if needed)
egress {
description = "HTTPS for external APIs"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "dmz-web-sg"
Tier = "dmz"
}
}DMZ Network ACL
resource "aws_network_acl" "dmz" {
vpc_id = aws_vpc.main.id
subnet_ids = [aws_subnet.dmz.id]
# Inbound rules
# Rule 100: Allow HTTPS
ingress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# Rule 110: Allow HTTP
ingress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# Rule 120: Allow responses from app tier
ingress {
rule_no = 120
protocol = "tcp"
action = "allow"
cidr_block = "10.0.2.0/24" # App subnet
from_port = 1024
to_port = 65535
}
# Rule 130: Allow ephemeral ports from Internet
ingress {
rule_no = 130
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# Rule 200: Deny known malicious IP
ingress {
rule_no = 200
protocol = "-1"
action = "deny"
cidr_block = "198.51.100.0/24"
from_port = 0
to_port = 0
}
# Outbound rules
# Rule 100: To app tier
egress {
rule_no = 100
protocol = "tcp"
action = "allow"
cidr_block = "10.0.2.0/24"
from_port = 8080
to_port = 8080
}
# Rule 110: HTTPS to Internet
egress {
rule_no = 110
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# Rule 120: Ephemeral ports to Internet
egress {
rule_no = 120
protocol = "tcp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# Rule 130: DNS
egress {
rule_no = 130
protocol = "udp"
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 53
to_port = 53
}
tags = {
Name = "dmz-nacl"
}
}On-Premise DMZ (nftables)
DMZ Firewall
#!/usr/sbin/nft -f
# DMZ firewall on web server
flush ruleset
table inet filter {
# App tier IPs (backend servers)
set app_tier {
type ipv4_addr
elements = { 10.0.2.10, 10.0.2.11, 10.0.2.12 }
}
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
# Accept HTTPS from Internet
tcp dport { 80, 443 } accept
# Accept SSH from bastion only (not from Internet)
tcp dport 22 ip saddr 10.0.1.5 accept
# Log dropped
log prefix "dmz-drop: " limit rate 5/minute
}
chain forward {
type filter hook forward priority 0; policy drop;
# No forwarding on DMZ server itself
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# Allow to app tier
tcp dport 8080 ip daddr @app_tier accept
# Allow DNS
udp dport 53 accept
# Allow HTTPS for external APIs (if needed)
tcp dport 443 accept
# Block everything else (including direct database access)
log prefix "dmz-egress-block: " limit rate 2/minute
}
}Monitoring DMZ Traffic
VPC Flow Logs
resource "aws_flow_log" "dmz" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.dmz_flow_logs.arn
tags = {
Name = "dmz-flow-logs"
}
}
resource "aws_cloudwatch_log_group" "dmz_flow_logs" {
name = "/aws/vpc/dmz-flow-logs"
retention_in_days = 30
}CloudWatch Alarms
# Alert on rejected connections to DMZ
resource "aws_cloudwatch_log_metric_filter" "dmz_rejected" {
name = "dmz-rejected-connections"
log_group_name = aws_cloudwatch_log_group.dmz_flow_logs.name
pattern = "[version, account, eni, source, destination, srcport, destport, protocol, packets, bytes, windowstart, windowend, action=REJECT, flowlogstatus]"
metric_transformation {
name = "DMZRejectedConnections"
namespace = "DMZ"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "dmz_rejected" {
alarm_name = "dmz-high-rejected-connections"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "DMZRejectedConnections"
namespace = "DMZ"
period = "300"
statistic = "Sum"
threshold = "100"
alarm_description = "High number of rejected connections to DMZ"
alarm_actions = [aws_sns_topic.alerts.arn]
}Best Practices
1. Principle of Least Privilege: DMZ can't directly access database 2. No Internet from Private Tiers: App/Data tiers use NAT Gateway for updates 3. Bastion for SSH: No direct SSH to DMZ from Internet 4. WAF for DMZ: Use AWS WAF or similar for web protection 5. IDS/IPS: Deploy intrusion detection in DMZ 6. Regular Audits: Review DMZ traffic patterns monthly 7. Separate Logging: Enhanced logging for DMZ tier 8. Automated Scanning: Regular vulnerability scans of DMZ instances
Common DMZ Mistakes
❌ DMZ to Database: DMZ should never directly connect to database ❌ No Egress Filtering: DMZ should have restricted outbound access ❌ Weak Monitoring: DMZ needs extra scrutiny ❌ Single Layer: DMZ needs both NACLs and Security Groups ❌ No WAF: Public-facing web servers should have WAF
DMZ vs Bastion Host
DMZ: Isolates public services (web, API) Bastion: Single entry point for administrative access
Often deployed together:
- DMZ: Public subnet with web servers
- Bastion: Public subnet with hardened jump box
- App/Data: Private subnets, accessible via bastion
Testing DMZ Configuration
# From Internet: Should access web services
curl https://dmz-server.example.com
# From DMZ: Should reach app tier
curl http://app-server:8080/health
# From DMZ: Should NOT reach database directly
telnet database-server 5432 # Should fail
# From App tier: Should reach database
psql -h database-server -U app_user -d mydb # Should succeedResources
- AWS VPC Design: https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario3.html
- For nftables patterns and AWS Security Groups, see the main SKILL.md
Egress Filtering Pattern
Control outbound traffic to prevent data exfiltration, malware communication, and unauthorized external access.
Table of Contents
- Why Egress Filtering?
- nftables Egress Filtering
- AWS Security Group Egress Restrictions
- Domain-Based Egress Filtering
- Squid Proxy Configuration
- Kubernetes Egress NetworkPolicy
- Monitoring Egress Traffic
- VPC Flow Logs Analysis
- nftables Egress Logging
- Best Practices
- Common Patterns
- Database Server (Minimal Egress)
- Application Server (Limited Egress)
- Testing Egress Rules
- Egress Filtering for Different Tiers
- Troubleshooting
- Resources
Why Egress Filtering?
Threats Mitigated:
- Data exfiltration by malware
- Command and control (C2) communication
- Unauthorized cloud storage uploads
- Crypto-mining to external pools
Default Approach: Deny all outbound, allow only necessary destinations.
nftables Egress Filtering
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
# Allowed external destinations
set allowed_external {
type ipv4_addr
flags interval
elements = {
52.4.48.0/24, # AWS API
140.82.112.0/20, # GitHub
151.101.0.0/16 # Fastly CDN (package repos)
}
}
# Allowed internal networks
set internal_networks {
type ipv4_addr
flags interval
elements = {
10.0.0.0/8,
172.16.0.0/12,
192.168.0.0/16
}
}
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
tcp dport { 22, 80, 443 } accept
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# Allow DNS (required for name resolution)
udp dport 53 accept
tcp dport 53 accept
# Allow to internal networks
ip daddr @internal_networks accept
# Allow to approved external destinations
ip daddr @allowed_external accept
# Allow HTTPS to specific domains (using IP sets)
# Note: For domain-based filtering, use external tools (Squid, etc.)
tcp dport 443 accept comment "HTTPS - add specific IPs above"
# Log blocked egress attempts
log prefix "egress-blocked: " limit rate 5/minute level warn
# Drop everything else
drop
}
}AWS Security Group Egress Restrictions
resource "aws_security_group" "restricted_egress" {
name = "restricted-egress-sg"
description = "Restrictive egress firewall"
vpc_id = aws_vpc.main.id
# NO default "allow all outbound" rule
# Explicit egress rules only
# Allow to internal VPC
egress {
description = "To VPC resources"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.main.cidr_block]
}
# Allow DNS
egress {
description = "DNS queries"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow HTTPS to specific service (e.g., AWS S3 endpoint)
egress {
description = "HTTPS to S3"
from_port = 443
to_port = 443
protocol = "tcp"
prefix_list_ids = [data.aws_prefix_list.s3.id]
}
# Allow to specific external API
egress {
description = "To external API"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["52.1.2.0/24"] # External API endpoint
}
tags = {
Name = "restricted-egress-sg"
}
}
# Use AWS managed prefix list for S3
data "aws_prefix_list" "s3" {
filter {
name = "prefix-list-name"
values = ["com.amazonaws.${var.region}.s3"]
}
}Domain-Based Egress Filtering
For domain-based control (e.g., allow github.com), use a proxy:
Squid Proxy Configuration
# Install Squid
sudo apt install squid
# Configure /etc/squid/squid.conf
acl allowed_domains dstdomain .github.com .npmjs.com .pypi.org
http_access allow allowed_domains
http_access deny all
# Restart Squid
sudo systemctl restart squidConfigure applications to use proxy:
export HTTP_PROXY=http://proxy-server:3128
export HTTPS_PROXY=http://proxy-server:3128Kubernetes Egress NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrictive-egress
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
# Allow to internal pods
- to:
- podSelector: {}
# Allow to specific external IP blocks
- to:
- ipBlock:
cidr: 52.1.2.0/24 # External API
ports:
- protocol: TCP
port: 443Monitoring Egress Traffic
VPC Flow Logs Analysis
-- CloudWatch Insights query for egress traffic
fields @timestamp, srcAddr, dstAddr, dstPort, action
| filter action = "ACCEPT" and dstAddr not like /^10\./
| filter dstAddr not like /^172\.(1[6-9]|2[0-9]|3[01])\./
| filter dstAddr not like /^192\.168\./
| stats count() by dstAddr, dstPort
| sort count desc
| limit 50nftables Egress Logging
# Add counter and log to egress rules
chain output {
# ... other rules
# Count and log blocked egress
counter name egress_blocked_counter
log prefix "egress-block: " flags all
drop
}View stats:
sudo nft list counter inet filter egress_blocked_counterBest Practices
1. Default Deny: Start with deny-all egress, allow explicitly 2. Allow DNS: Always allow DNS (port 53) or name resolution fails 3. Internal Networks: Allow RFC1918 private ranges 4. Use Prefix Lists: AWS managed prefix lists for services (S3, DynamoDB) 5. Log Blocked Traffic: Monitor for legitimate traffic being blocked 6. Regular Reviews: Audit allowed destinations quarterly 7. Domain-Based: Use proxy (Squid) for domain-based control 8. Network Segmentation: Different egress policies per tier
Common Patterns
Database Server (Minimal Egress)
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# DNS
udp dport 53 accept
# Internal network only
ip daddr 10.0.0.0/8 accept
# No Internet access at all
log prefix "db-egress-block: "
drop
}Application Server (Limited Egress)
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# DNS
udp dport 53 accept
# Internal
ip daddr 10.0.0.0/8 accept
# Specific external APIs
ip daddr { 52.1.2.0/24, 140.82.112.0/20 } tcp dport 443 accept
# Package repos (for updates)
ip daddr 151.101.0.0/16 tcp dport 443 accept
log prefix "app-egress-block: "
drop
}Testing Egress Rules
# Test DNS resolution (should work)
nslookup google.com
# Test allowed external IP (should work)
curl https://api.example.com
# Test blocked destination (should fail)
curl https://blocked-site.com
# Check logs for blocked attempts
sudo journalctl -k | grep "egress-block"Egress Filtering for Different Tiers
| Tier | Egress Policy |
|---|---|
| Web (DMZ) | Internal + specific APIs + package repos |
| App | Internal + database + external APIs |
| Database | Internal only (no Internet) |
| Bastion | Minimal (internal SSH only) |
Troubleshooting
Application fails after enabling egress filtering:
- Check logs for blocked connections
- Identify legitimate external dependencies
- Add to allow list
- Re-test
DNS resolution fails:
- Ensure UDP port 53 allowed
- Check if DNS server IP blocked
- Verify DNS server address:
cat /etc/resolv.conf
Package updates fail:
- Allow package repository IPs
- For apt: Allow
archive.ubuntu.com,security.ubuntu.com - For yum: Allow
mirror.centos.org
Resources
- For nftables patterns, AWS Security Groups, and Kubernetes NetworkPolicies, see the main SKILL.md
Related skills
FAQ
Which host firewall does it recommend for Ubuntu?
UFW for simplicity on Ubuntu/Debian, and nftables for advanced control on modern distros.
How does it prevent lockouts?
It emphasizes allowing SSH before enabling the firewall and using stateful rules that allow return traffic.