Skip to content
Web Server Security Hardening — TLS Best Practices, Security Headers, WAF, Rate Limiting, and Compliance

Web Server Security Hardening — TLS Best Practices, Security Headers, WAF, Rate Limiting, and Compliance

DodaTech Updated Jun 20, 2026 10 min read

Every day, automated scanners probe web servers for vulnerabilities — outdated TLS, missing headers, SQL injection points, and directory traversal. Hardening your web server closes these attack vectors before they become incidents. This guide covers TLS 1.3 best practices, security headers (CSP, HSTS, X-Frame-Options), ModSecurity WAF configuration, IP-based and endpoint-based rate limiting, bot detection, audit logging, and compliance checks for GDPR and PCI DSS.

What You’ll Learn

You’ll implement TLS 1.3 with modern cipher suites and HSTS, configure comprehensive security headers (CSP, XFO, XSS-Protection, Referrer-Policy), deploy ModSecurity with the OWASP Core Rule Set, rate-limit by IP and endpoint with burst handling, detect and block malicious bots, set up audit logging for compliance, and run security audits against your server. Durga Antivirus Pro uses these hardening techniques to protect its update distribution infrastructure.

Security Hardening Path

    flowchart LR
  A[DNS Management] --> B[TLS Configuration]
  B --> C[Security Headers]
  C --> D[WAF & Rate Limiting]
  D --> E[Bot Detection]
  E --> F[Audit & Compliance]
  F --> G[Security Hardening<br/>You are here]
  style G fill:#f90,color:#fff
  

TLS 1.3 Best Practices

# /etc/nginx/nginx.conf or /etc/nginx/sites-available/example.com
server {
    listen 443 ssl http2;
    server_name example.com;

    # Certificate chain
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS — only 1.2 and 1.3
    ssl_protocols TLSv1.2 TLSv1.3;

    # Secure ciphers (no RC4, 3DES, CBC, or export ciphers)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # DH parameters for perfect forward secrecy
    ssl_dhparam /etc/nginx/dhparam.pem;

    # Session management
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 4h;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # HSTS — tell browsers to always use HTTPS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

TLS Testing

# Check TLS configuration
curl -I https://example.com

# Check supported protocols and ciphers
nmap --script ssl-enum-ciphers -p 443 example.com

# Use SSL Labs test (requires public domain)
# https://www.ssllabs.com/ssltest/

# Grade A+ requirements:
# - TLS 1.2 and 1.3 only
# - Strong key exchange (ECDHE)
# - HSTS enabled
# - OCSP stapling enabled
# - Chain complete (no missing intermediates)

Security Headers

# /etc/nginx/nginx.conf — default headers for all sites
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" 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 Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;

# Content Security Policy (CSP) — per-site
# Start with report-only, then enforce
# add_header Content-Security-Policy-Report-Only "...";
add_header Content-Security-Policy "
    default-src 'self';
    script-src 'self' https://cdn.example.com;
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https://*.example.com;
    font-src 'self';
    connect-src 'self' https://api.example.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    upgrade-insecure-requests;
" always;

Headers Explained

HeaderPurposeRecommended Value
X-Content-Type-OptionsPrevent MIME type sniffingnosniff
X-Frame-OptionsPrevent clickjackingDENY
HSTSForce HTTPSmax-age=63072000; includeSubDomains
CSPPrevent XSS and data injectionPer-site policy
Referrer-PolicyControl referrer infostrict-origin-when-cross-origin
Permissions-PolicyRestrict browser featurescamera=(), microphone=()

ModSecurity WAF

# Install ModSecurity for NGINX
# Using libmodsecurity3 Connector
sudo apt install libmodsecurity3 -y
git clone https://github.com/SpiderLabs/ModSecurity-nginx.git

# Compile dynamic module
./configure --with-nginx=/path/to/nginx
make && sudo make install
# /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsecurity.conf;
}
# /etc/nginx/modsecurity.conf
SecRuleEngine On
SecRequestBodyLimit 13107200

# OWASP Core Rule Set
Include /etc/nginx/crs/rules/*.conf

# Custom rule: block path traversal
SecRule REQUEST_URI "@rx \.\./" \
    "id:3000,phase:1,deny,status:403"

# Custom rule: block sensitive paths
SecRule REQUEST_URI "@rx (wp-admin|phpmyadmin|\.env|\.git)" \
    "id:3001,phase:1,deny,status:403"

# Custom rule: SQL injection
SecRule ARGS "@rx (union.*select|select.*from|insert.*into)" \
    "id:3002,phase:2,deny,status:403"

# Audit log
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsecurity_audit.log

Rate Limiting

# NGINX rate limiting
http {
    # Zone definitions
    limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    limit_req_zone $http_x_forwarded_for zone=api:10m rate=100r/s;

    # Connection limiting
    limit_conn_zone $binary_remote_addr zone=conn:10m;
}

server {
    # General rate limit
    location / {
        limit_req zone=general burst=50 nodelay;
        limit_conn conn 20;
    }

    # Stricter rate for login
    location /api/login {
        limit_req zone=login burst=3 nodelay;
        limit_conn conn 5;
        # Return 429 with Retry-After
        limit_req_status 429;
        add_header Retry-After "60" always;
    }

    # API rate limit
    location /api/ {
        limit_req zone=api burst=150 nodelay;
        limit_conn conn 50;
    }
}
# Apache ModSecurity rate limiting (alternative)
# /etc/apache2/mods-available/security2.conf

# Track request counts per IP
SecAction "id:4000,phase:5,pass,\
    setvar:IP.REQUEST_COUNT=+1,\
    expirevar:IP.REQUEST_COUNT=60"

# Block if > 100 requests in 60 seconds
SecRule IP:REQUEST_COUNT "@gt 100" \
    "id:4001,phase:5,deny,status:429,\
     msg:'Rate limit exceeded',\
     setenv:RATE_LIMITED"

Bot Detection

# Block known bad bots by user-agent
map $http_user_agent $bad_bot {
    default 0;
    ~*(curl|wget|python-requests|go-http-client|scrapy|java) 1;
    ~*(masscan|zgrab|nmap|sqlmap|nikto|nessus|openvas) 1;
    ~*(ahrefs|majestic|semrush|archive.org|dotbot) 0;  # legitimate
}

server {
    if ($bad_bot) {
        return 403;
    }

    # Or return a fake page to waste attacker's time
    # if ($bad_bot) { rewrite ^ /honeypot.html last; }
}
# Geo-based blocking (optional)
# /etc/nginx/nginx.conf
geo $blocked_country {
    default 0;
    RU 1;
    CN 1;
    KP 1;
    IR 1;
}

server {
    if ($blocked_country) {
        return 403;
    }
}

Audit Logging

# NGINX audit log format
log_format audit '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '$request_time $upstream_response_time '
    '$request_id "$http_x_forwarded_for"';

server {
    # Detailed audit log for security monitoring
    access_log /var/log/nginx/audit.log audit;
    # Keep access log too
    access_log /var/log/nginx/access.log combined;
}
# Log monitoring with fail2ban
# /etc/fail2ban/jail.local
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600

[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 86400

[nginx-429]
enabled = true
port = http,https
filter = nginx-429
logpath = /var/log/nginx/access.log
maxretry = 10
bantime = 3600

Compliance Checks (GDPR, PCI DSS)

#!/bin/bash
# security-audit.sh — Automated security checks

echo "=== Web Server Security Audit ==="
echo "Target: ${1:-localhost}"
echo ""

TARGET=${1:-localhost}
PASS=0
FAIL=0

check() {
    if [ "$1" = true ]; then
        echo "  ✓ $2"
        ((PASS++))
    else
        echo "  ✗ $2"
        ((FAIL++))
    fi
}

# TLS version check (no TLS 1.0/1.1)
TLS_OK=$(nmap --script ssl-enum-ciphers -p 443 "$TARGET" 2>/dev/null | \
    grep -E "TLSv1\.0|TLSv1\.1" | wc -l)
check [ "$TLS_OK" -eq 0 ] "No TLS 1.0/1.1 (PCI DSS 4.1)"

# HSTS header
HSTS=$(curl -sI "https://$TARGET" | grep -i "strict-transport-security")
check [ -n "$HSTS" ] "HSTS header present"

# X-Frame-Options
XFO=$(curl -sI "https://$TARGET" | grep -i "x-frame-options")
check [ -n "$XFO" ] "X-Frame-Options present"

# CSP header
CSP=$(curl -sI "https://$TARGET" | grep -i "content-security-policy")
if [ -z "$CSP" ]; then
    CSP=$(curl -sI "https://$TARGET" | grep -i "content-security-policy-report-only")
fi
check [ -n "$CSP" ] "CSP header present (recommended)"

# Directory listing
LISTING=$(curl -s "http://$TARGET/images/" | grep -i "index of")
check [ -z "$LISTING" ] "Directory listing disabled"

# Server header information disclosure
SERVER=$(curl -sI "https://$TARGET" | grep -i "^server:" | grep -v "nginx/1" | head -1)
check [ -z "$SERVER" ] "Server version not exposed (set server_tokens off)"

# HTTPS redirect
HTTPS=$(curl -sI "http://$TARGET" 2>/dev/null | grep -i "301\|302.*https://")
check [ -n "$HTTPS" ] "HTTP → HTTPS redirect"

# Rate limiting test
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://$TARGET/api/login")
check [ "$STATUS" != "429" ] "Rate limiting configured"

echo ""
echo "=== Summary: ${PASS} passed, ${FAIL} failed ==="
[ "$FAIL" -eq 0 ] && echo "Security hardening OK" || echo "Remediate failed checks"

Common Security Mistakes

1. TLS 1.0/1.1 Still Enabled

PCI DSS 4.1 requires disabling TLS 1.0/1.1. Check: nmap --script ssl-enum-ciphers -p 443 example.com. Set ssl_protocols TLSv1.2 TLSv1.3;.

2. Missing HSTS Preload

Without HSTS, users can still connect via HTTP on their first visit (MITM risk). Submit to hstspreload.org after enabling: add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload".

3. CSP Too Permissive

default-src 'self' is safe but restrictive. Adding 'unsafe-inline' weakens XSS protection. Build CSP iteratively using Content-Security-Policy-Report-Only first.

4. Rate Limiting Without Burst

Without burst, every request over the limit returns 429 immediately, even legitimate bursts. Always set burst=N to absorb legitimate traffic spikes.

5. Server Version in Headers

Server: nginx/1.24.0 tells attackers your exact version and known vulnerabilities. Set server_tokens off; in NGINX or ServerTokens Prod in Apache.

6. Fail2Ban Not Configured

Fail2Ban parses logs and bans IPs after N failures. Without it, brute force attacks continue indefinitely. Configure jails for SSH, HTTP auth, and bot scanners.

7. Self-Signed Certificates in Production

Self-signed certs trigger browser warnings and train users to ignore them. Use Let’s Encrypt (free, auto-renewing) or a paid CA (DigiCert, Sectigo).

Practice Questions

1. What is the purpose of the Content-Security-Policy header? CSP restricts which resources (scripts, styles, images, fonts) the browser can load and which origins they can come from. It prevents XSS and data injection attacks.

2. How does OCSP stapling improve SSL security? OCSP stapling lets the server pre-fetch its certificate revocation status and include it in the TLS handshake. Without it, browsers must contact the CA directly, which may fail or leak user browsing data.

3. What is the difference between limit_req and limit_conn in NGINX? limit_req limits request rate (requests per second). limit_conn limits concurrent connections. Both prevent resource exhaustion but target different attack vectors.

4. Why should you use report-only mode for CSP initially? Report-only sends violation reports without blocking resources. It lets you discover what your site actually needs before enforcing policies that may break functionality.

5. Challenge: Design a layered security architecture that protects against DDoS, SQL injection, brute force login, and information disclosure. Answer: DDoS → Cloudflare/CDN with rate limiting. SQL injection → WAF (ModSecurity with OWASP CRS). Brute force → fail2ban + login rate limiting (5/min). Info disclosure → server_tokens off, no directory listing, proper error pages.

Mini Project: Security Hardening Script

#!/bin/bash
# harden-web-server.sh — Automated security hardening for NGINX

set -euo pipefail

echo "=== Web Server Hardening ==="

# 1. Disable server tokens
echo "[1/6] Disabling server tokens..."
if grep -q "server_tokens" /etc/nginx/nginx.conf; then
    sed -i 's/server_tokens.*/server_tokens off;/' /etc/nginx/nginx.conf
else
    echo "server_tokens off;" >> /etc/nginx/nginx.conf
fi

# 2. Add security headers
echo "[2/6] Adding security headers..."
cat > /etc/nginx/security-headers.conf << 'EOF'
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
EOF

# Include in http block
if ! grep -q "security-headers" /etc/nginx/nginx.conf; then
    sed -i '/^http {/a \    include /etc/nginx/security-headers.conf;' /etc/nginx/nginx.conf
fi

# 3. Configure rate limiting
echo "[3/6] Configuring rate limiting..."
cat > /etc/nginx/rate-limit.conf << 'EOF'
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
EOF

# 4. Set up fail2ban
echo "[4/6] Configuring fail2ban..."
cat > /etc/fail2ban/jail.local << 'EOF'
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600

[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 86400
EOF

# 5. Install and configure ModSecurity
echo "[5/6] Setting up ModSecurity..."
# (skipped if not available — install separately)

# 6. Test and reload
echo "[6/6] Testing configuration..."
nginx -t && systemctl reload nginx

echo "=== Hardening complete ==="
echo "Test at: https://www.ssllabs.com/ssltest/"

FAQ

How often should I rotate SSL certificates?
Let’s Encrypt certificates expire every 90 days. Certbot auto-renews via systemd timer. For paid CAs, aim for annual renewal before expiry.
What is the most important security header?
HSTS (Strict-Transport-Security) is the most impactful — it forces all future connections to HTTPS, preventing SSL stripping attacks. CSP is the most flexible but complex.
Can I use ModSecurity without the OWASP CRS?
The CRS provides comprehensive protection (SQL injection, XSS, path traversal, etc.). Without it, you’d need to write hundreds of custom rules. Always include the CRS.
How do I handle legitimate traffic that gets rate limited?
Add the client IP to a whitelist: geo $whitelist { default 0; 203.0.113.50 1; } and if ($whitelist) { limit_req off; }.
What is the difference between Symantec, DigiCert, and Let’s Encrypt?
Let’s Encrypt is free, automated, and trusted everywhere. DigiCert/Sectigo offer extended validation (EV) and longer validity. For most web servers, Let’s Encrypt is sufficient.
How do I detect if my server has been compromised?
Check for unknown processes (top, ps aux), unexpected outbound connections (netstat -tunp), modified system binaries (rkhunter), and unauthorized SSH keys.

What’s Next

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro