This commit is contained in:
Toby
2026-03-24 18:02:50 -04:00
parent 2d209b9258
commit ba9e489584
111 changed files with 48382 additions and 3099 deletions
+385 -16
View File
@@ -11,10 +11,35 @@ metadata:
Configure host-based and cloud firewalls for network security.
## When to Use This Skill
Use this skill when:
- Setting up a new server and need to restrict network access
- Implementing network segmentation between application tiers
- Configuring cloud security groups for AWS, GCP, or Azure resources
- Migrating from iptables to nftables
- Auditing existing firewall rules for compliance
- Responding to a security incident requiring emergency network blocks
## Prerequisites
- Root or sudo access on Linux hosts
- AWS CLI configured for cloud security groups
- Understanding of TCP/IP, ports, and protocols
- Network diagram showing required traffic flows
## iptables
### Basic Setup with Default Deny
```bash
# Default policies
# Flush existing rules
iptables -F
iptables -X
iptables -t nat -F
iptables -t mangle -F
# Default policies - deny all inbound, allow outbound
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
@@ -25,60 +50,404 @@ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow SSH
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# Allow HTTP/HTTPS
# Allow SSH (restrict to management subnet)
iptables -A INPUT -p tcp --dport 22 -s 10.0.100.0/24 -j ACCEPT
# Allow HTTP/HTTPS from anywhere
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
# Save rules
# Allow ICMP (ping) with rate limiting
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s --limit-burst 4 -j ACCEPT
# Log dropped packets (rate limited to avoid log flooding)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTABLES-DROP: " --log-level 4
# Save rules (Debian/Ubuntu)
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
```
### Anti-DDoS Rules
```bash
# SYN flood protection
iptables -A INPUT -p tcp --syn -m limit --limit 25/s --limit-burst 50 -j ACCEPT
iptables -A INPUT -p tcp --syn -j DROP
# Limit new connections per source IP
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 50 -j REJECT
# Block port scanning (detect TCP flags abuse)
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
iptables -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
iptables -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
```
### Application-Specific Rules
```bash
# Web server with database backend
# Allow app servers to reach database (port 5432)
iptables -A INPUT -p tcp --dport 5432 -s 10.0.1.0/24 -j ACCEPT
# Allow monitoring (Prometheus node exporter)
iptables -A INPUT -p tcp --dport 9100 -s 10.0.200.0/24 -j ACCEPT
# DNS resolution
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A INPUT -p tcp --sport 53 -j ACCEPT
# NTP
iptables -A INPUT -p udp --sport 123 -j ACCEPT
# Block specific IP (incident response)
iptables -I INPUT 1 -s 203.0.113.50 -j DROP
```
## UFW (Uncomplicated Firewall)
```bash
# Enable UFW with default deny
ufw default deny incoming
ufw default allow outgoing
ufw enable
# Allow SSH from management network
ufw allow from 10.0.100.0/24 to any port 22 proto tcp
# Allow HTTP/HTTPS
ufw allow 80/tcp
ufw allow 443/tcp
# Allow specific application profile
ufw allow 'Nginx Full'
# Rate limit SSH (max 6 connections in 30 seconds)
ufw limit ssh
# Allow port range
ufw allow 8000:8080/tcp
# Deny specific IP
ufw deny from 203.0.113.50
# Check status
ufw status verbose
ufw status numbered
# Delete a rule by number
ufw delete 3
# Application profiles
ufw app list
ufw app info 'Nginx Full'
```
## nftables
### Complete Server Configuration
```bash
#!/usr/sbin/nft -f
flush ruleset
# Define variables
define LAN = 10.0.0.0/16
define MGMT = 10.0.100.0/24
define MONITOR = 10.0.200.0/24
table inet filter {
# Rate limiting set
set rate_limit {
type ipv4_addr
flags dynamic,timeout
timeout 1m
}
chain input {
type filter hook input priority 0; policy drop;
# Connection tracking
ct state established,related accept
ct state invalid drop
# Loopback
iif "lo" accept
tcp dport { 22, 80, 443 } accept
# ICMP and ICMPv6
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } limit rate 10/second accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert } accept
# SSH from management only
tcp dport 22 ip saddr $MGMT accept
# HTTP/HTTPS from anywhere
tcp dport { 80, 443 } accept
# Prometheus metrics from monitoring subnet
tcp dport 9100 ip saddr $MONITOR accept
# Rate limit new connections
tcp flags syn limit rate over 25/second burst 50 packets drop
# Log dropped traffic
log prefix "nft-drop: " level warn limit rate 5/minute
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
# Optional: restrict outbound to known destinations
# tcp dport { 80, 443, 53 } accept
# udp dport { 53, 123 } accept
# ct state established,related accept
# drop
}
}
# NAT table for port forwarding
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
# Forward port 8080 to internal app server
tcp dport 8080 dnat to 10.0.1.10:8080
}
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth0" masquerade
}
}
```
## AWS Security Groups
### nftables Management Commands
```bash
aws ec2 create-security-group --group-name web-sg --description "Web server SG"
# Load configuration
nft -f /etc/nftables.conf
aws ec2 authorize-security-group-ingress \
# List all rules
nft list ruleset
# List specific table
nft list table inet filter
# Add a rule dynamically
nft add rule inet filter input tcp dport 8443 accept
# Insert rule at position
nft insert rule inet filter input position 5 ip saddr 10.0.50.0/24 tcp dport 3306 accept
# Delete a rule by handle
nft -a list chain inet filter input # show handles
nft delete rule inet filter input handle 15
# Monitor in real time
nft monitor
```
## AWS Security Groups
### Terraform Configuration
```hcl
# Web tier security group
resource "aws_security_group" "web" {
name_prefix = "web-sg-"
vpc_id = aws_vpc.main.id
description = "Security group for web servers"
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP redirect"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg"
Environment = "production"
ManagedBy = "terraform"
}
}
# App tier - only accepts traffic from web tier
resource "aws_security_group" "app" {
name_prefix = "app-sg-"
vpc_id = aws_vpc.main.id
description = "Security group for application servers"
ingress {
description = "HTTP from web tier"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Database tier - only accepts from app tier
resource "aws_security_group" "db" {
name_prefix = "db-sg-"
vpc_id = aws_vpc.main.id
description = "Security group for database servers"
ingress {
description = "PostgreSQL from app tier"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
### AWS CLI Commands
```bash
# Create security group
aws ec2 create-security-group \
--group-name web-sg \
--description "Web server SG" \
--vpc-id vpc-0abc123
# Add inbound rule
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc123 \
--protocol tcp --port 443 \
--cidr 0.0.0.0/0
# Add rule referencing another security group
aws ec2 authorize-security-group-ingress \
--group-id sg-0db456 \
--protocol tcp --port 5432 \
--source-group sg-0app789
# Remove a rule
aws ec2 revoke-security-group-ingress \
--group-id sg-0abc123 \
--protocol tcp --port 22 \
--cidr 0.0.0.0/0
# Describe rules
aws ec2 describe-security-group-rules \
--filters Name=group-id,Values=sg-0abc123
```
## Firewall Rule Audit Script
```bash
#!/bin/bash
# firewall-audit.sh - Audit current firewall rules for common issues
echo "=== Firewall Audit Report ==="
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Host: $(hostname)"
echo ""
# Check if firewall is active
if command -v nft &>/dev/null; then
echo "--- nftables rules ---"
nft list ruleset
elif command -v iptables &>/dev/null; then
echo "--- iptables rules ---"
iptables -L -n -v --line-numbers
fi
echo ""
echo "--- Open ports ---"
ss -tlnp
echo ""
echo "--- Potential issues ---"
# Check for overly permissive rules
if iptables -L INPUT -n 2>/dev/null | grep -q "0.0.0.0/0.*dpt:22"; then
echo "WARNING: SSH (port 22) open to 0.0.0.0/0 - restrict to management subnet"
fi
if iptables -L INPUT -n 2>/dev/null | grep -q "0.0.0.0/0.*dpt:3306"; then
echo "CRITICAL: MySQL (port 3306) open to 0.0.0.0/0"
fi
if iptables -L INPUT -n 2>/dev/null | grep -q "0.0.0.0/0.*dpt:5432"; then
echo "CRITICAL: PostgreSQL (port 5432) open to 0.0.0.0/0"
fi
# Check default policies
DEFAULT_INPUT=$(iptables -L INPUT 2>/dev/null | head -1 | grep -oP 'policy \K\w+')
if [ "$DEFAULT_INPUT" = "ACCEPT" ]; then
echo "CRITICAL: Default INPUT policy is ACCEPT - should be DROP"
fi
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Locked out of SSH | Rule order or default deny applied before allow | Use out-of-band console access; add SSH allow rule first |
| Rules lost after reboot | Rules not persisted | Install `iptables-persistent` or save to `/etc/nftables.conf` |
| Docker bypasses iptables | Docker modifies iptables FORWARD chain | Use `DOCKER-USER` chain for custom rules; set `"iptables": false` in daemon.json |
| nftables and iptables conflict | Both running simultaneously | Migrate fully to nftables; remove iptables packages |
| AWS SG rule limit reached | Max 60 inbound rules per SG | Use prefix lists or consolidate CIDR ranges |
| Legitimate traffic blocked | Rule ordering issue | Place more specific allow rules before general deny rules |
## Best Practices
- Default deny policy
- Minimal rule sets
- Regular rule audits
- Log denied traffic
- Document all rules
- Default deny policy on all chains
- Minimal rule sets - only open what is required
- Regular rule audits (monthly minimum)
- Log denied traffic for security monitoring
- Document all rules with descriptions and ticket references
- Use connection tracking for stateful inspection
- Rate limit inbound connections to prevent DDoS
- Separate management traffic from application traffic
- Test rule changes in staging before production
- Keep persistent backups of working rule sets
## Related Skills
- [linux-hardening](../../hardening/linux-hardening/) - System security
- [aws-vpc](../../../infrastructure/cloud-aws/aws-vpc/) - AWS networking
- [zero-trust](../zero-trust/) - Identity-based access patterns
- [vpn-setup](../vpn-setup/) - Secure tunnel configuration
+423 -33
View File
@@ -9,25 +9,157 @@ metadata:
# SSL/TLS Management
Manage certificates and secure communications.
Manage certificates and secure communications across web servers, Kubernetes clusters, and internal services.
## Let's Encrypt (Certbot)
## When to Use This Skill
Use this skill when:
- Setting up HTTPS for a new web application
- Automating certificate renewal with Let's Encrypt
- Deploying cert-manager in Kubernetes
- Configuring TLS for internal service-to-service communication
- Auditing cipher suites and TLS versions for compliance
- Responding to an expiring or compromised certificate
## Prerequisites
- Domain name with DNS control for public certificates
- Root/sudo access on web servers
- `certbot` installed for Let's Encrypt
- `openssl` CLI available (installed by default on most Linux distros)
- Kubernetes cluster with Helm for cert-manager deployment
- Understanding of X.509 certificate chain of trust
## Let's Encrypt with Certbot
### Installation and Certificate Issuance
```bash
# Install
apt install certbot python3-certbot-nginx
# Install certbot (Ubuntu/Debian)
apt update && apt install -y certbot python3-certbot-nginx
# Get certificate
# Obtain certificate for nginx (interactive)
certbot --nginx -d example.com -d www.example.com
# Auto-renewal
certbot renew --dry-run
# Cron: 0 0 * * * certbot renew --quiet
# Non-interactive mode for automation
certbot certonly --nginx \
-d example.com \
-d www.example.com \
--non-interactive \
--agree-tos \
--email admin@example.com
# Standalone mode (when no web server is running)
certbot certonly --standalone \
-d example.com \
--preferred-challenges http
# DNS challenge (for wildcard certs)
certbot certonly --manual \
--preferred-challenges dns \
-d "*.example.com" \
-d example.com
# Using DNS plugin for automation (Cloudflare example)
pip install certbot-dns-cloudflare
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.example.com" \
-d example.com
```
## cert-manager (Kubernetes)
### Renewal Automation
```bash
# Test renewal
certbot renew --dry-run
# Systemd timer (preferred over cron)
cat > /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot certificate renewal
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
EOF
cat > /etc/systemd/system/certbot-renewal.timer << 'EOF'
[Unit]
Description=Run certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl enable --now certbot-renewal.timer
# Verify timer is active
systemctl list-timers certbot-renewal.timer
# Renewal hooks for post-renewal actions
mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh << 'HOOK'
#!/bin/bash
systemctl reload nginx
# Also reload other services using the cert
systemctl reload haproxy 2>/dev/null || true
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
```
## cert-manager for Kubernetes
### Installation
```bash
# Install with Helm
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.0 \
--set installCRDs=true \
--set prometheus.enabled=true
# Verify installation
kubectl get pods -n cert-manager
kubectl get crds | grep cert-manager
```
### ClusterIssuer Configurations
```yaml
# letsencrypt-staging (use for testing first)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
---
# letsencrypt-prod
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
@@ -39,58 +171,316 @@ spec:
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
- http01:
ingress:
class: nginx
---
# DNS challenge solver (for wildcard certs with Cloudflare)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-dns
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
# Self-signed CA issuer for internal services
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-ca
spec:
selfSigned: {}
```
### Certificate Resources
```yaml
# Public-facing certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-cert
namespace: default
spec:
secretName: example-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- example.com
- www.example.com
duration: 2160h # 90 days
renewBefore: 720h # 30 days before expiry
---
# Wildcard certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: default
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-prod-dns
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.com
---
# Ingress with automatic TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- example.com
secretName: example-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
```
## Strong Configuration
## OpenSSL Commands Reference
```bash
# Generate a private key
openssl genrsa -out server.key 4096
# Generate an ECDSA key (preferred for performance)
openssl ecparam -genkey -name prime256v1 -out server-ec.key
# Generate a CSR (Certificate Signing Request)
openssl req -new -key server.key -out server.csr \
-subj "/C=US/ST=California/L=San Francisco/O=Acme Corp/CN=example.com"
# Generate CSR with SAN (Subject Alternative Names)
openssl req -new -key server.key -out server.csr -config <(cat <<EOF
[req]
default_bits = 4096
distinguished_name = dn
req_extensions = san
prompt = no
[dn]
CN = example.com
O = Acme Corp
C = US
[san]
subjectAltName = DNS:example.com,DNS:www.example.com,DNS:api.example.com
EOF
)
# Generate self-signed certificate (development/testing)
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout selfsigned.key -out selfsigned.crt \
-subj "/CN=localhost"
# View certificate details
openssl x509 -in cert.pem -noout -text
# Check certificate expiration date
openssl x509 -in cert.pem -noout -dates
# Check remote certificate
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -dates -subject -issuer
# Verify certificate chain
openssl verify -CAfile ca-bundle.crt server.crt
# Check certificate chain from remote server
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -text
# Convert PEM to PKCS12
openssl pkcs12 -export -out cert.pfx -inkey server.key -in server.crt -certfile ca.crt
# Convert PKCS12 to PEM
openssl pkcs12 -in cert.pfx -out cert.pem -nodes
# Test TLS connection and cipher negotiation
openssl s_client -connect example.com:443 -tls1_3
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
```
## Strong TLS Configuration
### Nginx
```nginx
# nginx ssl config
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_stapling on;
ssl_stapling_verify on;
server {
listen 443 ssl http2;
server_name example.com;
add_header Strict-Transport-Security "max-age=63072000" always;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Protocol versions
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites (TLS 1.2)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
# HTTP to HTTPS redirect (in separate server block)
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
```
### Apache
```apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLUseStapling on
SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</VirtualHost>
```
## Certificate Monitoring
```bash
# Check expiration
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -dates
#!/bin/bash
# cert-monitor.sh - Monitor certificate expiration across hosts
# Check certificate chain
openssl s_client -connect example.com:443 -showcerts
WARN_DAYS=30
CRIT_DAYS=7
HOSTS=(
"example.com:443"
"api.example.com:443"
"admin.example.com:443"
)
for host in "${HOSTS[@]}"; do
expiry=$(echo | openssl s_client -connect "$host" -servername "${host%%:*}" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "ERROR: Cannot connect to $host"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -le "$CRIT_DAYS" ]; then
echo "CRITICAL: $host expires in $days_left days ($expiry)"
elif [ "$days_left" -le "$WARN_DAYS" ]; then
echo "WARNING: $host expires in $days_left days ($expiry)"
else
echo "OK: $host expires in $days_left days ($expiry)"
fi
done
```
### Prometheus cert-manager Metrics
```yaml
# Alert on expiring certificates in Kubernetes
groups:
- name: cert-manager
rules:
- alert: CertificateExpiringSoon
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600
for: 1h
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} expires in less than 7 days"
- alert: CertificateNotReady
expr: certmanager_certificate_ready_status{condition="True"} == 0
for: 15m
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} is not ready"
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Certbot fails with "connection refused" | Port 80 blocked by firewall | Open port 80 for ACME HTTP-01 challenge |
| "Too many certificates already issued" | Let's Encrypt rate limit hit | Use staging endpoint for testing; wait for rate limit reset |
| cert-manager challenge stuck pending | Ingress or DNS misconfigured | Check `kubectl describe challenge`; verify DNS records |
| Mixed content warnings | HTTP resources on HTTPS page | Update all asset URLs to HTTPS; use CSP headers |
| OCSP stapling not working | Resolver not configured | Add `resolver` directive in nginx; verify outbound DNS |
| Intermediate cert missing | Incomplete chain served | Use `fullchain.pem` not `cert.pem`; verify with `openssl s_client -showcerts` |
| TLS handshake failure | Client doesn't support offered ciphers | Add TLS 1.2 support; check cipher suite compatibility |
## Best Practices
- Automate renewal
- Monitor expiration
- Use strong ciphers
- Enable HSTS
- Regular security audits
- Automate renewal with systemd timers or cert-manager
- Monitor expiration dates with alerting (30-day and 7-day warnings)
- Use only TLS 1.2 and TLS 1.3
- Enable HSTS with long max-age and includeSubDomains
- Enable OCSP stapling to improve handshake performance
- Use ECDSA keys for better performance where possible
- Test configuration with SSL Labs (ssllabs.com/ssltest)
- Keep private keys secure with proper file permissions (0600)
- Rotate certificates before expiry, not after
- Maintain a certificate inventory across all services
## Related Skills
- [hashicorp-vault](../../secrets/hashicorp-vault/) - PKI management
- [waf-setup](../waf-setup/) - Web protection
- [zero-trust](../zero-trust/) - mTLS and identity-based access
+374 -22
View File
@@ -11,65 +11,417 @@ metadata:
Configure secure VPN tunnels for remote access and site connectivity.
## When to Use This Skill
Use this skill when:
- Setting up secure remote access for employees or contractors
- Connecting on-premises networks to cloud environments (site-to-site)
- Encrypting traffic between data centers or regions
- Implementing a mesh VPN for distributed infrastructure
- Providing secure access to internal services without exposing them publicly
## Prerequisites
- Linux server with a public IP for VPN endpoint
- Root/sudo access on the VPN server
- Firewall rules allowing VPN traffic (UDP 51820 for WireGuard, UDP 1194 for OpenVPN)
- DNS configured for VPN hostname (optional but recommended)
- Understanding of IP subnetting and routing
## WireGuard
```bash
# Generate keys
wg genkey | tee privatekey | wg pubkey > publickey
### Server Setup
# Server config (/etc/wireguard/wg0.conf)
```bash
# Install WireGuard (Ubuntu/Debian)
apt update && apt install -y wireguard
# Generate server keys
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
# Generate pre-shared key (optional, adds post-quantum resistance)
wg genpsk > /etc/wireguard/psk.key
chmod 600 /etc/wireguard/psk.key
```
### Server Configuration
```ini
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
# Enable IP forwarding and NAT on startup
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# DNS for clients
DNS = 10.0.0.1
# Peer: Alice (laptop)
[Peer]
PublicKey = <client-public-key>
PublicKey = <alice-public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.0.0.2/32
# Enable
# Peer: Bob (mobile)
[Peer]
PublicKey = <bob-public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.0.0.3/32
# Peer: Office network (site-to-site)
[Peer]
PublicKey = <office-public-key>
PresharedKey = <preshared-key>
AllowedIPs = 10.0.0.4/32, 192.168.1.0/24
Endpoint = office.example.com:51820
PersistentKeepalive = 25
```
### Client Configuration
```ini
# Client config: alice.conf
[Interface]
Address = 10.0.0.2/24
PrivateKey = <alice-private-key>
DNS = 10.0.0.1
[Peer]
PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
```
### Split Tunneling Configuration
```ini
# Client config with split tunnel (only route internal traffic through VPN)
[Interface]
Address = 10.0.0.2/24
PrivateKey = <alice-private-key>
# No DNS override for split tunnel
[Peer]
PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = vpn.example.com:51820
# Only route specific subnets through VPN
AllowedIPs = 10.0.0.0/24, 172.16.0.0/16, 192.168.1.0/24
PersistentKeepalive = 25
```
### WireGuard Management Commands
```bash
# Start/stop interface
wg-quick up wg0
wg-quick down wg0
# Enable on boot
systemctl enable wg-quick@wg0
# Show connection status
wg show
wg show wg0
# Add a new peer dynamically
wg set wg0 peer <new-public-key> allowed-ips 10.0.0.5/32
# Remove a peer
wg set wg0 peer <public-key> remove
# Show transfer statistics
wg show wg0 transfer
# Generate QR code for mobile clients
apt install -y qr-encode
qrencode -t ansiutf8 < alice-mobile.conf
```
### Peer Management Script
```bash
#!/bin/bash
# wg-add-peer.sh - Add a new WireGuard peer
set -euo pipefail
PEER_NAME="${1:?Usage: $0 <peer-name>}"
SERVER_CONF="/etc/wireguard/wg0.conf"
CLIENTS_DIR="/etc/wireguard/clients"
SERVER_PUBKEY=$(cat /etc/wireguard/server_public.key)
SERVER_ENDPOINT="vpn.example.com:51820"
PSK=$(cat /etc/wireguard/psk.key)
# Find next available IP
LAST_IP=$(grep -oP 'AllowedIPs = 10\.0\.0\.\K[0-9]+' "$SERVER_CONF" | sort -n | tail -1)
NEXT_IP=$((LAST_IP + 1))
mkdir -p "$CLIENTS_DIR"
# Generate client keys
wg genkey | tee "$CLIENTS_DIR/${PEER_NAME}_private.key" | wg pubkey > "$CLIENTS_DIR/${PEER_NAME}_public.key"
chmod 600 "$CLIENTS_DIR/${PEER_NAME}_private.key"
CLIENT_PRIVKEY=$(cat "$CLIENTS_DIR/${PEER_NAME}_private.key")
CLIENT_PUBKEY=$(cat "$CLIENTS_DIR/${PEER_NAME}_public.key")
# Add peer to server config
cat >> "$SERVER_CONF" << EOF
# Peer: ${PEER_NAME}
[Peer]
PublicKey = ${CLIENT_PUBKEY}
PresharedKey = ${PSK}
AllowedIPs = 10.0.0.${NEXT_IP}/32
EOF
# Generate client config
cat > "$CLIENTS_DIR/${PEER_NAME}.conf" << EOF
[Interface]
Address = 10.0.0.${NEXT_IP}/24
PrivateKey = ${CLIENT_PRIVKEY}
DNS = 10.0.0.1
[Peer]
PublicKey = ${SERVER_PUBKEY}
PresharedKey = ${PSK}
Endpoint = ${SERVER_ENDPOINT}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
# Reload WireGuard
wg syncconf wg0 <(wg-quick strip wg0)
echo "Peer ${PEER_NAME} added with IP 10.0.0.${NEXT_IP}"
echo "Client config: ${CLIENTS_DIR}/${PEER_NAME}.conf"
```
## OpenVPN
```bash
# Install
apt install openvpn easy-rsa
### Server Setup
# Generate certificates
```bash
# Install OpenVPN and Easy-RSA
apt install -y openvpn easy-rsa
# Initialize PKI
make-cadir /etc/openvpn/easy-rsa
cd /etc/openvpn/easy-rsa
./easyrsa init-pki
./easyrsa build-ca
./easyrsa build-ca nopass
./easyrsa gen-req server nopass
./easyrsa sign-req server server
./easyrsa gen-dh
openvpn --genkey secret /etc/openvpn/ta.key
# Generate client certificate
./easyrsa gen-req client1 nopass
./easyrsa sign-req client client1
```
### Server Configuration
```ini
# /etc/openvpn/server.conf
port 1194
proto udp
dev tun
ca /etc/openvpn/easy-rsa/pki/ca.crt
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
key /etc/openvpn/easy-rsa/pki/private/server.key
dh /etc/openvpn/easy-rsa/pki/dh.pem
tls-auth /etc/openvpn/ta.key 0
server 10.8.0.0 255.255.255.0
# Route client traffic through VPN
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 8.8.8.8"
push "dhcp-option DNS 8.8.4.4"
# Split tunnel: push specific routes instead
# push "route 172.16.0.0 255.255.0.0"
# push "route 192.168.1.0 255.255.255.0"
keepalive 10 120
# Cipher and auth
cipher AES-256-GCM
auth SHA256
data-ciphers AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305
# Hardening
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384
user nobody
group nogroup
persist-key
persist-tun
# Logging
status /var/log/openvpn/status.log
log-append /var/log/openvpn/openvpn.log
verb 3
# Max clients
max-clients 100
# Client isolation (clients cannot see each other)
client-to-client
```
### Client Configuration
```ini
# client1.ovpn
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client1.crt
key client1.key
tls-auth ta.key 1
cipher AES-256-GCM
auth SHA256
verb 3
```
## Tailscale (Managed WireGuard)
```bash
# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
# Authenticate and connect
tailscale up
# Advertise subnet routes (act as a gateway)
tailscale up --advertise-routes=192.168.1.0/24,172.16.0.0/16
# Enable as exit node (route all traffic)
tailscale up --advertise-exit-node
# Use an exit node
tailscale up --exit-node=<exit-node-ip>
# Check status
tailscale status
# Access control: tailscale ACL policy (in admin console)
# Example ACL policy
cat << 'EOF'
{
"acls": [
{"action": "accept", "src": ["group:engineering"], "dst": ["tag:servers:*"]},
{"action": "accept", "src": ["group:devops"], "dst": ["*:*"]},
{"action": "accept", "src": ["tag:monitoring"], "dst": ["tag:servers:9100"]}
],
"tagOwners": {
"tag:servers": ["group:devops"],
"tag:monitoring": ["group:devops"]
},
"groups": {
"group:engineering": ["alice@example.com", "bob@example.com"],
"group:devops": ["charlie@example.com"]
}
}
EOF
# Enable MagicDNS and set DNS
tailscale up --accept-dns
# SSH via Tailscale (no SSH keys needed)
tailscale up --ssh
```
## AWS Site-to-Site VPN
```bash
aws ec2 create-vpn-gateway --type ipsec.1
aws ec2 create-customer-gateway \
# Create Virtual Private Gateway
VGW_ID=$(aws ec2 create-vpn-gateway --type ipsec.1 --query 'VpnGateway.VpnGatewayId' --output text)
# Attach to VPC
aws ec2 attach-vpn-gateway --vpn-gateway-id "$VGW_ID" --vpc-id vpc-0abc123
# Create Customer Gateway (your on-prem device)
CGW_ID=$(aws ec2 create-customer-gateway \
--type ipsec.1 \
--bgp-asn 65000 \
--public-ip <on-prem-ip>
aws ec2 create-vpn-connection \
--public-ip 203.0.113.10 \
--query 'CustomerGateway.CustomerGatewayId' --output text)
# Create VPN connection
VPN_ID=$(aws ec2 create-vpn-connection \
--type ipsec.1 \
--customer-gateway-id cgw-xxx \
--vpn-gateway-id vgw-xxx
--customer-gateway-id "$CGW_ID" \
--vpn-gateway-id "$VGW_ID" \
--options '{"StaticRoutesOnly":false}' \
--query 'VpnConnection.VpnConnectionId' --output text)
# Download configuration for your device
aws ec2 describe-vpn-connections --vpn-connection-ids "$VPN_ID"
# Enable route propagation
aws ec2 enable-vgw-route-propagation \
--gateway-id "$VGW_ID" \
--route-table-id rtb-0abc123
# Monitor VPN tunnel status
aws ec2 describe-vpn-connections \
--vpn-connection-ids "$VPN_ID" \
--query 'VpnConnections[0].VgwTelemetry'
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| WireGuard handshake never completes | Firewall blocking UDP 51820 | Open UDP 51820 on server firewall and any intermediate firewalls |
| No internet through VPN | IP forwarding disabled or NAT missing | Enable `net.ipv4.ip_forward=1`; verify PostUp iptables rules |
| DNS not resolving over VPN | DNS not pushed or local DNS conflicts | Set `DNS = ` in client config; check `/etc/resolv.conf` |
| OpenVPN TLS handshake fails | Certificate mismatch or expired | Verify CA cert matches; check certificate dates with `openssl x509 -dates` |
| Split tunnel leaks traffic | AllowedIPs too broad | Set only specific subnets in AllowedIPs; verify with `traceroute` |
| Tailscale node unreachable | ACL blocking traffic | Check Tailscale admin ACL policy; verify node is online with `tailscale status` |
| AWS VPN tunnel flapping | Idle timeout or DPD misconfigured | Enable DPD on customer gateway; send periodic keep-alive traffic |
| Slow VPN performance | MTU issues causing fragmentation | Set `MTU = 1420` in WireGuard config; test with `ping -M do -s 1400` |
## Best Practices
- Use WireGuard for modern deployments
- Implement MFA for VPN access
- Regular key rotation
- Monitor VPN connections
- Segment VPN access by role
- Use WireGuard for modern deployments (simpler, faster, smaller attack surface)
- Implement MFA for VPN access where possible
- Rotate keys regularly (quarterly for WireGuard, annual for OpenVPN certs)
- Monitor VPN connections and alert on anomalies
- Segment VPN access by role using split tunneling or ACLs
- Use pre-shared keys with WireGuard for post-quantum resistance
- Keep VPN software updated to patch security vulnerabilities
- Log all VPN connection events for audit purposes
- Disable VPN access immediately when employees leave
- Test failover for site-to-site VPN connections
## Related Skills
- [zero-trust](../zero-trust/) - Modern access patterns
- [ssl-tls-management](../ssl-tls-management/) - Certificate management
- [firewall-config](../firewall-config/) - Network access control
+481 -37
View File
@@ -11,69 +11,513 @@ metadata:
Protect web applications with Web Application Firewalls.
## When to Use This Skill
Use this skill when:
- Deploying a public-facing web application that needs attack protection
- Meeting compliance requirements (PCI-DSS, SOC2) for web application security
- Blocking OWASP Top 10 attack categories (SQLi, XSS, CSRF, etc.)
- Protecting APIs from abuse, injection, and rate-based attacks
- Adding a virtual patching layer while application code is being fixed
## Prerequisites
- Web application behind a load balancer or reverse proxy
- AWS account for AWS WAF, or Cloudflare account for Cloudflare WAF
- Nginx with ModSecurity module compiled for self-hosted WAF
- Access to application logs to tune rules and identify false positives
- Understanding of HTTP request/response structure
## AWS WAF
### Create Web ACL with Managed Rules
```bash
# Create Web ACL
# Create Web ACL with AWS managed rules
aws wafv2 create-web-acl \
--name my-waf \
--name production-waf \
--scope REGIONAL \
--default-action Allow={} \
--rules file://rules.json
# Associate with ALB
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:... \
--resource-arn arn:aws:elasticloadbalancing:...
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=production-waf \
--rules file://waf-rules.json
```
## ModSecurity (nginx)
### AWS WAF Rules Configuration
```nginx
# nginx.conf
load_module modules/ngx_http_modsecurity_module.so;
server {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
}
```json
[
{
"Name": "AWSManagedRulesCommonRuleSet",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesCommonRuleSet",
"ExcludedRules": []
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSCommonRules"
}
},
{
"Name": "AWSManagedRulesSQLiRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesSQLiRuleSet"
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSSQLiRules"
}
},
{
"Name": "AWSManagedRulesKnownBadInputsRuleSet",
"Priority": 3,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesKnownBadInputsRuleSet"
}
},
"OverrideAction": { "None": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "AWSBadInputRules"
}
},
{
"Name": "RateLimitRule",
"Priority": 4,
"Statement": {
"RateBasedStatement": {
"Limit": 2000,
"AggregateKeyType": "IP"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RateLimit"
}
},
{
"Name": "GeoBlockRule",
"Priority": 5,
"Statement": {
"GeoMatchStatement": {
"CountryCodes": ["KP", "IR", "SY"]
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "GeoBlock"
}
},
{
"Name": "BlockBadUserAgents",
"Priority": 6,
"Statement": {
"ByteMatchStatement": {
"SearchString": "sqlmap",
"FieldToMatch": { "SingleHeader": { "Name": "user-agent" } },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BadUserAgent"
}
}
]
```
### Associate WAF with ALB
```bash
# Install OWASP CRS
git clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/crs
# Associate with Application Load Balancer
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123
# Associate with API Gateway
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:apigateway:us-east-1::/restapis/abc123/stages/prod
```
### AWS WAF Terraform
```hcl
resource "aws_wafv2_web_acl" "main" {
name = "production-waf"
scope = "REGIONAL"
description = "Production WAF with OWASP protections"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 1
override_action { none {} }
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
rule_action_override {
name = "SizeRestrictions_BODY"
action_to_use { count {} }
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSCommonRules"
sampled_requests_enabled = true
}
}
rule {
name = "RateLimit"
priority = 10
action { block {} }
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "production-waf"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "alb" {
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
```
## Cloudflare WAF
### API Configuration
```bash
# Enable managed rules via API
curl -X PUT "https://api.cloudflare.com/client/v4/zones/{zone}/firewall/waf/packages/{package}/rules/{rule}" \
-H "Authorization: Bearer $TOKEN" \
-d '{"mode":"block"}'
# List available WAF rulesets
curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
-H "Authorization: Bearer ${CF_TOKEN}" | jq '.result[] | {id, name, phase}'
# Create a custom WAF rule
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Custom WAF Rules",
"kind": "zone",
"phase": "http_request_firewall_custom",
"rules": [
{
"action": "block",
"expression": "(http.request.uri.query contains \"union select\" or http.request.uri.query contains \"1=1\")",
"description": "Block SQL injection patterns in query string"
},
{
"action": "block",
"expression": "(http.request.uri.path contains \"..%2f\" or http.request.uri.path contains \"..%5c\")",
"description": "Block path traversal attempts"
},
{
"action": "challenge",
"expression": "(cf.threat_score gt 30)",
"description": "Challenge high threat score visitors"
},
{
"action": "block",
"expression": "(http.request.headers[\"user-agent\"] contains \"sqlmap\" or http.request.headers[\"user-agent\"] contains \"nikto\")",
"description": "Block known attack tools"
}
]
}'
# Configure rate limiting
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Rate Limiting",
"kind": "zone",
"phase": "http_ratelimit",
"rules": [
{
"action": "block",
"ratelimit": {
"characteristics": ["ip.src"],
"period": 60,
"requests_per_period": 100,
"mitigation_timeout": 600
},
"expression": "(http.request.uri.path matches \"^/api/\")",
"description": "Rate limit API endpoints"
}
]
}'
```
## Common Rules
### Cloudflare Terraform
```yaml
protections:
- SQL Injection (SQLi)
- Cross-Site Scripting (XSS)
- Remote File Inclusion (RFI)
- Local File Inclusion (LFI)
- Command Injection
- Cross-Site Request Forgery (CSRF)
```hcl
resource "cloudflare_ruleset" "waf_custom" {
zone_id = var.zone_id
name = "Custom WAF Rules"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = "(http.request.uri.query contains \"union select\")"
description = "Block SQL injection in query string"
}
rules {
action = "managed_challenge"
expression = "(cf.threat_score gt 30)"
description = "Challenge suspicious visitors"
}
}
```
## ModSecurity with Nginx
### Installation
```bash
# Install ModSecurity for Nginx (Ubuntu)
apt install -y libmodsecurity3 libmodsecurity-dev nginx libnginx-mod-http-modsecurity
# Or compile from source
git clone https://github.com/SpiderLabs/ModSecurity /opt/modsecurity
cd /opt/modsecurity
git submodule init && git submodule update
./build.sh && ./configure && make && make install
```
### Nginx Configuration
```nginx
# /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so;
http {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
server {
listen 443 ssl http2;
server_name example.com;
# ModSecurity can also be enabled per-location
location /api/ {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/api-rules.conf;
proxy_pass http://backend;
}
}
}
```
### ModSecurity Main Configuration
```bash
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
# Set to DetectionOnly first, switch to On after tuning
SecRuleEngine On
# Request body handling
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Response body handling
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
# Logging
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial
SecAuditLog /var/log/modsec/modsec_audit.log
# Include OWASP Core Rule Set
Include /etc/nginx/modsec/crs/crs-setup.conf
Include /etc/nginx/modsec/crs/rules/*.conf
```
### OWASP Core Rule Set Setup
```bash
# Download and install OWASP CRS
cd /etc/nginx/modsec
git clone https://github.com/coreruleset/coreruleset crs
cp crs/crs-setup.conf.example crs/crs-setup.conf
# Customize CRS settings
cat >> crs/crs-setup.conf << 'EOF'
# Set paranoia level (1-4, higher = more strict)
SecAction "id:900000, phase:1, pass, t:none, nolog, setvar:tx.paranoia_level=2"
# Set anomaly score thresholds
SecAction "id:900110, phase:1, pass, t:none, nolog, \
setvar:tx.inbound_anomaly_score_threshold=5, \
setvar:tx.outbound_anomaly_score_threshold=4"
# Exclude known false positives
SecRule REQUEST_URI "@beginsWith /api/upload" \
"id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=920420"
EOF
# Create rule exclusions file
cat > /etc/nginx/modsec/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf << 'EOF'
# Exclude rules that cause false positives on specific paths
SecRule REQUEST_URI "@beginsWith /api/webhook" \
"id:1000001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:payload"
# Exclude rules for specific parameters
SecRule ARGS_NAMES "^content$" \
"id:1000002,phase:1,pass,nolog,ctl:ruleRemoveTargetById=941100;ARGS:content"
EOF
```
### Custom ModSecurity Rules
```bash
# /etc/nginx/modsec/custom-rules.conf
# Block requests with known attack tool user agents
SecRule REQUEST_HEADERS:User-Agent "@pm sqlmap nikto nmap masscan dirbuster" \
"id:10001,phase:1,deny,status:403,log,msg:'Blocked attack tool'"
# Block requests to sensitive paths
SecRule REQUEST_URI "@rx /(\.git|\.env|\.svn|wp-admin|phpmyadmin|adminer)" \
"id:10002,phase:1,deny,status:404,log,msg:'Blocked sensitive path access'"
# Rate limit by IP (10 requests/second)
SecRule IP:REQUEST_RATE "@gt 10" \
"id:10003,phase:1,deny,status:429,log,msg:'Rate limit exceeded',\
setvar:IP.request_rate=+1,expirevar:IP.request_rate=1"
# Block oversized cookies (potential overflow attack)
SecRule REQUEST_HEADERS:Cookie "@gt 4096" \
"id:10004,phase:1,deny,status:400,log,msg:'Oversized cookie header'"
# Virtual patch: block specific CVE exploit pattern
SecRule ARGS:filename "@contains ../../" \
"id:10005,phase:2,deny,status:403,log,msg:'Path traversal blocked (virtual patch CVE-XXXX-XXXX)'"
# Require Content-Type on POST requests
SecRule REQUEST_METHOD "@streq POST" \
"id:10006,phase:1,chain,deny,status:400,log,msg:'POST without Content-Type'"
SecRule &REQUEST_HEADERS:Content-Type "@eq 0" ""
```
## WAF Tuning Workflow
```bash
#!/bin/bash
# waf-tune.sh - Analyze WAF logs for false positives
AUDIT_LOG="/var/log/modsec/modsec_audit.log"
TIMEFRAME="24h"
echo "=== WAF Tuning Report ==="
echo "Analyzing last ${TIMEFRAME} of audit logs"
echo ""
# Top blocked rules
echo "--- Top 10 triggered rules ---"
grep -oP 'id "\K[0-9]+' "$AUDIT_LOG" | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Top blocked URIs ---"
grep -oP 'REQUEST_URI: \K[^\s]+' "$AUDIT_LOG" | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Top blocked IPs ---"
grep -oP 'client \K[0-9.]+' "$AUDIT_LOG" | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- False positive candidates (high-frequency blocks on common paths) ---"
grep -oP 'id "\K[0-9]+' "$AUDIT_LOG" | sort | uniq -c | sort -rn | \
while read count rule_id; do
if [ "$count" -gt 100 ]; then
echo " Rule $rule_id triggered $count times - review for false positive"
fi
done
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Legitimate requests blocked | False positives from CRS rules | Set `SecRuleEngine DetectionOnly` first; review audit log; add exclusions |
| WAF not blocking attacks | Rules in detection-only mode | Switch `SecRuleEngine On` after tuning period |
| High latency with WAF enabled | Response body inspection overhead | Disable `SecResponseBodyAccess` if not needed; reduce `paranoia_level` |
| AWS WAF rules not matching | Rule priority order wrong | Lower priority number = evaluated first; reorder rules |
| ModSecurity crashes nginx | Memory exhaustion on large requests | Increase `SecRequestBodyLimit`; adjust `SecPcreMatchLimit` |
| Cloudflare WAF blocks API calls | Expression too broad | Narrow expression with path or method conditions |
| CRS update breaks application | New rules trigger on existing traffic | Pin CRS version; test updates in staging first |
## Best Practices
- Start in detection mode
- Tune for false positives
- Monitor blocked requests
- Regular rule updates
- Custom rules for app-specific attacks
- Start in detection/log mode, switch to blocking after tuning
- Tune rules for at least 1-2 weeks before enforcement
- Monitor blocked requests daily during tuning phase
- Update managed rule sets and CRS regularly
- Create custom rules for application-specific attack patterns
- Use virtual patching to protect against known CVEs while code is being fixed
- Set appropriate rate limits per endpoint
- Maintain exclusion rules documentation with justifications
- Test WAF rules with known attack payloads before deploying
- Keep audit logs for at least 90 days for forensic analysis
## Related Skills
- [dast-scanning](../../scanning/dast-scanning/) - Web security testing
- [ssl-tls-management](../ssl-tls-management/) - HTTPS configuration
- [firewall-config](../firewall-config/) - Network-level firewalling
+377 -28
View File
@@ -11,76 +11,425 @@ metadata:
Implement "never trust, always verify" security model.
## When to Use This Skill
Use this skill when:
- Replacing traditional perimeter-based VPN access models
- Implementing BeyondCorp-style access to internal applications
- Securing multi-cloud or hybrid-cloud environments
- Enforcing identity-based access for every service interaction
- Meeting compliance requirements for continuous verification and least privilege
- Adopting micro-segmentation for Kubernetes or cloud workloads
## Prerequisites
- Identity provider (IdP) supporting OIDC/SAML (Okta, Azure AD, Google Workspace)
- Service mesh or proxy infrastructure (Istio, Envoy, Cloudflare Access)
- Device management/MDM solution for device posture checks
- Kubernetes cluster for workload-level examples
- Understanding of mTLS, RBAC, and network policies
## Core Principles
```yaml
zero_trust_principles:
- Verify explicitly (authenticate all access)
- Least privilege access
- Assume breach (micro-segmentation)
- Continuous validation
- End-to-end encryption
verify_explicitly:
description: "Authenticate and authorize every access request"
controls:
- Strong multi-factor authentication
- Identity-aware proxy for all applications
- Service-to-service mTLS
- API token validation on every request
least_privilege:
description: "Grant minimum access needed for the task"
controls:
- Just-in-time (JIT) access provisioning
- Time-bounded access grants
- Role-based access with fine-grained permissions
- Regular access reviews and certification
assume_breach:
description: "Design systems expecting compromise has occurred"
controls:
- Micro-segmentation between all services
- End-to-end encryption (data in transit and at rest)
- Continuous monitoring and anomaly detection
- Blast radius containment
```
## Identity-Based Access
## BeyondCorp Implementation
### Cloudflare Access Configuration
```bash
# Create an Access application for an internal service
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/apps" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Internal Dashboard",
"domain": "dashboard.internal.example.com",
"type": "self_hosted",
"session_duration": "12h",
"auto_redirect_to_identity": true,
"allowed_idps": ["google-workspace-idp-id"]
}'
# Create an Access policy
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/access/apps/${APP_ID}/policies" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Engineering team access",
"decision": "allow",
"include": [
{ "group": { "id": "engineering-group-id" } }
],
"require": [
{ "login_method": { "id": "google-workspace-idp-id" } }
],
"exclude": [
{ "geo": { "country_code": "KP" } }
]
}'
# Create a device posture rule
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/devices/posture" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Require disk encryption",
"type": "disk_encryption",
"match": { "platform": "linux" },
"schedule": "1h",
"input": { "requireAll": true }
}'
```
### Cloudflare Access Terraform
```hcl
resource "cloudflare_access_application" "dashboard" {
account_id = var.cloudflare_account_id
name = "Internal Dashboard"
domain = "dashboard.internal.example.com"
type = "self_hosted"
session_duration = "12h"
auto_redirect_to_identity = true
}
resource "cloudflare_access_policy" "engineering" {
account_id = var.cloudflare_account_id
application_id = cloudflare_access_application.dashboard.id
name = "Engineering team"
precedence = 1
decision = "allow"
include {
group = [cloudflare_access_group.engineering.id]
}
require {
login_method = [var.google_idp_id]
}
}
resource "cloudflare_access_group" "engineering" {
account_id = var.cloudflare_account_id
name = "Engineering"
include {
email_domain = ["example.com"]
}
require {
group = ["engineering@example.com"]
}
}
```
## Identity-Aware Proxy with OAuth2 Proxy
```yaml
# Service mesh mTLS
# oauth2-proxy deployment for protecting internal services
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
namespace: auth
spec:
replicas: 2
selector:
matchLabels:
app: oauth2-proxy
template:
metadata:
labels:
app: oauth2-proxy
spec:
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
args:
- --provider=oidc
- --oidc-issuer-url=https://accounts.google.com
- --client-id=$(CLIENT_ID)
- --client-secret=$(CLIENT_SECRET)
- --email-domain=example.com
- --upstream=http://internal-service.default.svc:8080
- --http-address=0.0.0.0:4180
- --cookie-secret=$(COOKIE_SECRET)
- --cookie-secure=true
- --cookie-httponly=true
- --cookie-samesite=lax
- --set-xauthrequest=true
- --pass-access-token=true
- --skip-provider-button=true
- --session-store-type=redis
- --redis-connection-url=redis://redis.auth.svc:6379
env:
- name: CLIENT_ID
valueFrom:
secretKeyRef:
name: oauth2-proxy
key: client-id
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy
key: client-secret
- name: COOKIE_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy
key: cookie-secret
ports:
- containerPort: 4180
---
# Ingress routing through oauth2-proxy
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: internal-service
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/oauth2/start?rd=$scheme://$host$request_uri"
nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-Request-User,X-Auth-Request-Email"
spec:
rules:
- host: dashboard.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: internal-service
port:
number: 8080
```
## Service Mesh mTLS (Istio)
```yaml
# Enforce strict mTLS across the mesh
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
---
# Authorization policy: frontend can call backend
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: frontend-to-backend
name: backend-access
namespace: default
spec:
selector:
matchLabels:
app: backend
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/frontend"]
- from:
- source:
principals: ["cluster.local/ns/default/sa/frontend"]
to:
- operation:
methods: ["GET", "POST"]
paths: ["/api/*"]
---
# Default deny all in namespace
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: production
spec: {}
```
## Network Segmentation
## Micro-Segmentation with Kubernetes Network Policies
```yaml
# Kubernetes Network Policy
# Default deny all traffic in namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
- Ingress
- Egress
---
# Allow DNS resolution for all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# Frontend: allow ingress from ingress controller, egress to backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-policy
namespace: production
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 8080
---
# Database: allow from backend only, no egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-policy
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
```
## OPA Policy for Access Decisions
```rego
# policy.rego - Zero trust access decision
package zerotrust.access
import rego.v1
default allow := false
allow if {
identity_verified
device_compliant
authorized_for_resource
risk_acceptable
}
identity_verified if {
input.identity.authenticated == true
input.identity.mfa_verified == true
time.now_ns() < input.identity.session_expires_ns
}
device_compliant if {
input.device.encryption_enabled == true
input.device.os_updated == true
input.device.firewall_enabled == true
input.device.certificate_valid == true
}
authorized_for_resource if {
some role in input.identity.roles
some permission in data.role_permissions[role]
permission == input.resource.required_permission
}
risk_acceptable if {
input.risk.score < 70
not input.risk.active_threat
}
step_up_required if {
input.risk.score >= 50
input.risk.score < 70
not input.identity.recent_mfa
}
```
## Implementation Steps
1. Identify sensitive resources
2. Map access patterns
3. Implement strong authentication
4. Apply micro-segmentation
5. Enable logging and monitoring
6. Continuous verification
1. **Inventory assets and data flows** - Map every application, service, and data store
2. **Deploy identity provider** - Centralize authentication with SSO and MFA
3. **Implement identity-aware proxy** - Route all access through authentication layer
4. **Enable mTLS for service mesh** - Encrypt and authenticate all service communication
5. **Apply network policies** - Default deny with explicit allow rules
6. **Add device posture checks** - Verify device compliance before granting access
7. **Deploy continuous monitoring** - Log and analyze all access decisions
8. **Iterate and refine** - Review policies based on monitoring data
## Best Practices
## Troubleshooting
- Identity-aware proxies
- Device trust verification
- Context-based access
- Encrypted communications
- Continuous monitoring
| Problem | Cause | Solution |
|---------|-------|----------|
| Users cannot access internal apps | Identity provider misconfigured | Verify OIDC/SAML settings; check redirect URIs |
| mTLS connections failing | Certificate expired or wrong CA | Check cert expiry with `istioctl proxy-config secret`; verify CA chain |
| Network policy blocking legitimate traffic | Missing egress or ingress rule | Use `kubectl describe networkpolicy`; verify pod labels match selectors |
| Device posture check fails | MDM agent not reporting | Verify device agent is running; check compliance dashboard |
| OAuth2 proxy returns 403 | User email domain not in allow-list | Add domain to `--email-domain` flag or update group membership |
## Related Skills
- [service-mesh](../../../infrastructure/networking/service-mesh/) - mTLS implementation
- [kubernetes-hardening](../../hardening/kubernetes-hardening/) - K8s security
- [vpn-setup](../vpn-setup/) - Traditional VPN (contrast with zero trust)