Chapter 16 — Security & Hardening

Security is not a feature you add later — it's a practice woven into every layer. This chapter covers the essential security measures for any production website.

SSL/TLS — Encrypting Traffic

How TLS Works (Simplified)

Client Server │ │ │──── ClientHello (supported ciphers) ───▶│ │◀─── ServerHello (chosen cipher) ────────│ │◀─── Certificate (public key) ───────────│ │ │ │ Client verifies certificate chain: │ │ Server cert → Intermediate CA → Root CA│ │ │ │──── Key Exchange (encrypted) ──────────▶│ │ │ │◀═══ Encrypted communication begins ═══▶│ │ (symmetric encryption, fast) │

Certificate Options

ProviderCostValidationBest For
Let's EncryptFreeDomain Validation (DV)Everything (90-day auto-renewal)
CloudflareFree (with proxy)DVSites behind Cloudflare
AWS ACMFree (with AWS services)DVAWS ALB/CloudFront
Commercial CAs$10-1000/yrOV/EVEnterprise, legal requirements

HSTS (HTTP Strict Transport Security)

# Force browsers to always use HTTPS (add to Nginx/response headers)
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

# Once set, browsers will NEVER make HTTP requests to your domain
# Submit to HSTS preload list: https://hstspreload.org/

Firewall Configuration

# UFW (Uncomplicated Firewall) — Ubuntu/Debian
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp      # SSH (or your custom port)
sudo ufw allow 80/tcp      # HTTP
sudo ufw allow 443/tcp     # HTTPS
sudo ufw enable
sudo ufw status verbose

# iptables (lower level, more control)
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow SSH, HTTP, HTTPS
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Drop everything else
iptables -A INPUT -j DROP

# Save rules (persist across reboot)
sudo apt install iptables-persistent
sudo netfilter-persistent save

SSH Hardening

# /etc/ssh/sshd_config — Production SSH configuration
Port 2222                          # Non-standard port (reduces noise)
PermitRootLogin no                 # Never allow root SSH
PasswordAuthentication no          # Keys only
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy                  # Whitelist specific users
Protocol 2

# Optional: Restrict to specific IPs (if you have static IP)
# AllowUsers deploy@YOUR_IP

Security Headers

# Add to Nginx server block or application responses:
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

# Test your headers: https://securityheaders.com/

Secrets Management

Never store secrets in: Git repositories, Dockerfiles, environment variables in CI logs, plain text config files committed to version control. Even if the repo is private — credentials in Git history are permanent.
ToolTypeBest For
Environment variablesRuntime injectionSimple apps (loaded from secure source)
AWS Secrets ManagerCloud-managedAWS workloads, auto-rotation
HashiCorp VaultSelf-hosted/cloudMulti-cloud, dynamic secrets, PKI
SOPSEncrypted files in GitGitOps workflows, small teams
Doppler / 1PasswordSaaSTeams wanting simple UI
# Example: Using SOPS to encrypt secrets in Git
# Install: brew install sops age

# Generate an age key
age-keygen -o keys.txt
# Public key: age1xxxxxxx...

# Create .sops.yaml in repo root
creation_rules:
  - path_regex: \.enc\.yaml$
    age: age1xxxxxxx...

# Encrypt a secrets file
sops --encrypt secrets.yaml > secrets.enc.yaml
# secrets.enc.yaml is safe to commit — encrypted at rest

# Decrypt at deploy time
sops --decrypt secrets.enc.yaml > /etc/myapp.env

Backup Strategy — The 3-2-1 Rule

# Automated database backup script
#!/bin/bash
# /opt/scripts/backup-db.sh
set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backups"
S3_BUCKET="s3://myapp-backups-prod"

# Dump database
pg_dump -h localhost -U myapp myapp_prod | gzip > "$BACKUP_DIR/db_$TIMESTAMP.sql.gz"

# Upload to S3 (offsite copy)
aws s3 cp "$BACKUP_DIR/db_$TIMESTAMP.sql.gz" "$S3_BUCKET/db/$TIMESTAMP.sql.gz"

# Retain only last 7 local backups
ls -t $BACKUP_DIR/db_*.sql.gz | tail -n +8 | xargs rm -f

# Cron: Run daily at 3 AM
# 0 3 * * * /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1
Test your backups! A backup you've never restored is not a backup — it's a hope. Schedule monthly restore tests to a separate environment. Automate this if possible.

DDoS Mitigation

Production Security Checklist

Before going live, verify:
☐ HTTPS everywhere (HSTS enabled)
☐ SSH: key-only auth, non-standard port, root disabled
☐ Firewall: only required ports open
☐ Security headers configured
☐ Secrets not in Git (use secrets manager)
☐ Database not publicly accessible
☐ Automatic security updates enabled
☐ Fail2ban or equivalent running
☐ Backups automated and tested
☐ Dependencies scanned for vulnerabilities (npm audit, Snyk)
☐ Application logs don't contain sensitive data
☐ Rate limiting on authentication endpoints
☐ CORS configured correctly (not wildcard in production)