mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user