Skip to content

Intermediate Level Nginx : Part 4

21: Setting Up Multiple Domains on One Server

Why Host Multiple Domains?

Running multiple websites on a single server: - Cost-effective: One server for multiple sites - Easier management: Centralized configuration - Resource efficiency: Share server resources - Simplified maintenance: Single point of updates

Basic Multi-Domain Setup

# Domain 1
server {
    listen 80;
    server_name domain1.com www.domain1.com;

    root /var/www/domain1.com;
    index index.html;

    access_log /var/log/nginx/domain1.access.log;
    error_log /var/log/nginx/domain1.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

# Domain 2
server {
    listen 80;
    server_name domain2.com www.domain2.com;

    root /var/www/domain2.com;
    index index.html;

    access_log /var/log/nginx/domain2.access.log;
    error_log /var/log/nginx/domain2.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

# Domain 3
server {
    listen 80;
    server_name domain3.com www.domain3.com;

    root /var/www/domain3.com;
    index index.html;

    access_log /var/log/nginx/domain3.access.log;
    error_log /var/log/nginx/domain3.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

Directory Structure

/var/www/
├── domain1.com/
│   ├── public/
│   │   ├── index.html
│   │   ├── css/
│   │   └── js/
│   └── logs/
├── domain2.com/
│   ├── public/
│   │   ├── index.html
│   │   ├── css/
│   │   └── js/
│   └── logs/
└── domain3.com/
    ├── public/
    │   ├── index.html
    │   ├── css/
    │   └── js/
    └── logs/

Create directories:

# Create directory structure
for domain in domain1.com domain2.com domain3.com; do
    sudo mkdir -p /var/www/$domain/{public,logs}
    sudo chown -R www-data:www-data /var/www/$domain
    sudo chmod -R 755 /var/www/$domain
done

# Create sample index files
for domain in domain1.com domain2.com domain3.com; do
    echo "<h1>Welcome to $domain</h1>" | sudo tee /var/www/$domain/public/index.html
done

Organized Configuration Files

Use sites-available and sites-enabled pattern:

# Configuration files structure
/etc/nginx/
├── nginx.conf
├── sites-available/
│   ├── domain1.com
│   ├── domain2.com
│   └── domain3.com
└── sites-enabled/
    ├── domain1.com -> ../sites-available/domain1.com
    ├── domain2.com -> ../sites-available/domain2.com
    └── domain3.com -> ../sites-available/domain3.com

Create and enable sites:

# Create configuration file
sudo nano /etc/nginx/sites-available/domain1.com

# Enable site (create symbolic link)
sudo ln -s /etc/nginx/sites-available/domain1.com /etc/nginx/sites-enabled/

# Disable site (remove symbolic link)
sudo rm /etc/nginx/sites-enabled/domain1.com

# Test configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

Multiple Domains with SSL/TLS

# Domain 1 with SSL
server {
    listen 80;
    server_name domain1.com www.domain1.com;
    return 301 https://domain1.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name www.domain1.com;

    ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;

    return 301 https://domain1.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name domain1.com;

    root /var/www/domain1.com/public;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;

    # SSL configuration
    include snippets/ssl-params.conf;

    access_log /var/log/nginx/domain1.access.log;
    error_log /var/log/nginx/domain1.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

# Domain 2 with SSL
server {
    listen 80;
    server_name domain2.com www.domain2.com;
    return 301 https://domain2.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name www.domain2.com;

    ssl_certificate /etc/letsencrypt/live/domain2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain2.com/privkey.pem;

    return 301 https://domain2.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name domain2.com;

    root /var/www/domain2.com/public;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/domain2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain2.com/privkey.pem;

    include snippets/ssl-params.conf;

    access_log /var/log/nginx/domain2.access.log;
    error_log /var/log/nginx/domain2.error.log;

    location / {
        try_files $uri $uri/ =404;
    }
}

Obtain SSL Certificates for Multiple Domains

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate for domain1
sudo certbot --nginx -d domain1.com -d www.domain1.com

# Obtain certificate for domain2
sudo certbot --nginx -d domain2.com -d www.domain2.com

# Obtain certificate for domain3
sudo certbot --nginx -d domain3.com -d www.domain3.com

# Or obtain all at once
sudo certbot --nginx -d domain1.com -d www.domain1.com -d domain2.com -d www.domain2.com -d domain3.com -d www.domain3.com

Shared SSL Configuration Snippet

Create reusable SSL configuration:

/etc/nginx/snippets/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

Use in server blocks:

server {
    listen 443 ssl http2;
    server_name domain1.com;

    ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;

    # Include shared SSL configuration
    include snippets/ssl-params.conf;

    # Rest of configuration
}

Subdomains

# Main domain
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/example.com/public;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/ssl-params.conf;
}

# Blog subdomain
server {
    listen 443 ssl http2;
    server_name blog.example.com;

    root /var/www/example.com/blog;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/ssl-params.conf;
}

# API subdomain
server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/ssl-params.conf;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Static assets subdomain
server {
    listen 443 ssl http2;
    server_name static.example.com;

    root /var/www/example.com/static;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/ssl-params.conf;

    location ~* \.(jpg|jpeg|png|gif|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Wildcard SSL Certificate

# Obtain wildcard certificate (requires DNS validation)
sudo certbot certonly --manual --preferred-challenges=dns -d example.com -d *.example.com

# Follow instructions to add DNS TXT record
# _acme-challenge.example.com TXT "validation-string"

Use wildcard certificate for all subdomains:

server {
    listen 443 ssl http2;
    server_name ~^(?<subdomain>.+)\.example\.com$;

    # Same certificate for all subdomains
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/ssl-params.conf;

    # Route to different directories based on subdomain
    root /var/www/example.com/$subdomain;

    location / {
        try_files $uri $uri/ =404;
    }
}

Different Applications on Different Domains

# Static site (domain1.com)
server {
    listen 443 ssl http2;
    server_name domain1.com www.domain1.com;

    root /var/www/domain1.com/public;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain1.com/privkey.pem;
    include snippets/ssl-params.conf;

    location / {
        try_files $uri $uri/ =404;
    }
}

# Node.js app (domain2.com)
server {
    listen 443 ssl http2;
    server_name domain2.com www.domain2.com;

    ssl_certificate /etc/letsencrypt/live/domain2.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain2.com/privkey.pem;
    include snippets/ssl-params.conf;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

# WordPress site (domain3.com)
server {
    listen 443 ssl http2;
    server_name domain3.com www.domain3.com;

    root /var/www/domain3.com/public;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/domain3.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain3.com/privkey.pem;
    include snippets/ssl-params.conf;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

# Python/Django app (domain4.com)
server {
    listen 443 ssl http2;
    server_name domain4.com www.domain4.com;

    ssl_certificate /etc/letsencrypt/live/domain4.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain4.com/privkey.pem;
    include snippets/ssl-params.conf;

    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /static/ {
        alias /var/www/domain4.com/static/;
    }

    location /media/ {
        alias /var/www/domain4.com/media/;
    }
}

Default Server Block (Catch-All)

Handle requests for undefined domains:

# Default server (catches all undefined domains)
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;

    server_name _;

    # Self-signed certificate for default server
    ssl_certificate /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;

    # Return 444 (close connection)
    return 444;
}

# Or return a custom error page
server {
    listen 80 default_server;
    server_name _;

    root /var/www/default;

    location / {
        return 404 '<h1>Site not found</h1>';
        add_header Content-Type text/html;
    }
}

Server Name Matching Order

Nginx matches server names in this order:

  1. Exact name
  2. Longest wildcard starting with asterisk (*.example.com)
  3. Longest wildcard ending with asterisk (mail.*)
  4. First matching regular expression
  5. Default server
# 1. Exact match
server {
    server_name example.com;
}

# 2. Wildcard at start
server {
    server_name *.example.com;
}

# 3. Wildcard at end
server {
    server_name mail.*;
}

# 4. Regular expression
server {
    server_name ~^(?<user>.+)\.example\.net$;
}

# 5. Default server
server {
    listen 80 default_server;
    server_name _;
}

Shared Configuration Snippets

Create reusable snippets for common configurations:

/etc/nginx/snippets/common-locations.conf
location = /favicon.ico {
    log_not_found off;
    access_log off;
}

location = /robots.txt {
    log_not_found off;
    access_log off;
}

location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
}

location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}
/etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" 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;

Use in server blocks:

server {
    listen 443 ssl http2;
    server_name example.com;

    # Include snippets
    include snippets/ssl-params.conf;
    include snippets/security-headers.conf;
    include snippets/common-locations.conf;

    location / {
        try_files $uri $uri/ =404;
    }
}

Resource Limits Per Domain

# Domain with higher limits
server {
    listen 443 ssl http2;
    server_name premium-domain.com;

    ssl_certificate /etc/letsencrypt/live/premium-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/premium-domain.com/privkey.pem;

    # Higher limits
    client_max_body_size 100m;
    limit_conn addr 100;
    limit_req zone=premium burst=100 nodelay;

    location / {
        proxy_pass http://backend;
    }
}

# Domain with standard limits
server {
    listen 443 ssl http2;
    server_name standard-domain.com;

    ssl_certificate /etc/letsencrypt/live/standard-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/standard-domain.com/privkey.pem;

    # Standard limits
    client_max_body_size 10m;
    limit_conn addr 10;
    limit_req zone=standard burst=20 nodelay;

    location / {
        proxy_pass http://backend;
    }
}

Testing Multi-Domain Setup

# Test configuration
sudo nginx -t

# Test specific domain resolution
curl -I http://domain1.com
curl -I https://domain1.com

# Test with specific Host header (if DNS not configured)
curl -H "Host: domain1.com" http://your-server-ip

# Test SSL certificate
openssl s_client -connect domain1.com:443 -servername domain1.com

# Test default server
curl -H "Host: nonexistent.com" http://your-server-ip

# Check which config file is being used
sudo nginx -T | grep server_name

Managing Multiple Domains Script

#!/bin/bash
# nginx-domain-manager.sh

SITES_AVAILABLE="/etc/nginx/sites-available"
SITES_ENABLED="/etc/nginx/sites-enabled"
WEBROOT="/var/www"

function create_site() {
    DOMAIN=$1

    # Create directory structure
    mkdir -p "$WEBROOT/$DOMAIN"/{public,logs}

    # Create sample index
    cat > "$WEBROOT/$DOMAIN/public/index.html" <<EOF
<!DOCTYPE html>
<html>
<head>
    <title>Welcome to $DOMAIN</title>
</head>
<body>
    <h1>$DOMAIN is working!</h1>
</body>
</html>
EOF

    # Create Nginx configuration
    cat > "$SITES_AVAILABLE/$DOMAIN" <<EOF
server {
    listen 80;
    server_name $DOMAIN www.$DOMAIN;

    root $WEBROOT/$DOMAIN/public;
    index index.html;

    access_log $WEBROOT/$DOMAIN/logs/access.log;
    error_log $WEBROOT/$DOMAIN/logs/error.log;

    location / {
        try_files \$uri \$uri/ =404;
    }
}
EOF

    # Set permissions
    chown -R www-data:www-data "$WEBROOT/$DOMAIN"
    chmod -R 755 "$WEBROOT/$DOMAIN"

    echo "Site created: $DOMAIN"
}

function enable_site() {
    DOMAIN=$1
    ln -s "$SITES_AVAILABLE/$DOMAIN" "$SITES_ENABLED/"
    echo "Site enabled: $DOMAIN"
}

function disable_site() {
    DOMAIN=$1
    rm "$SITES_ENABLED/$DOMAIN"
    echo "Site disabled: $DOMAIN"
}

function delete_site() {
    DOMAIN=$1
    disable_site $DOMAIN
    rm "$SITES_AVAILABLE/$DOMAIN"
    rm -rf "$WEBROOT/$DOMAIN"
    echo "Site deleted: $DOMAIN"
}

function list_sites() {
    echo "Available sites:"
    ls -1 "$SITES_AVAILABLE"
    echo ""
    echo "Enabled sites:"
    ls -1 "$SITES_ENABLED"
}

# Main menu
case "$1" in
    create)
        create_site $2
        ;;
    enable)
        enable_site $2
        ;;
    disable)
        disable_site $2
        ;;
    delete)
        delete_site $2
        ;;
    list)
        list_sites
        ;;
    *)
        echo "Usage: $0 {create|enable|disable|delete|list} [domain]"
        exit 1
        ;;
esac

# Test and reload
nginx -t && systemctl reload nginx

Usage:

# Make script executable
chmod +x nginx-domain-manager.sh

# Create new site
sudo ./nginx-domain-manager.sh create example.com

# Enable site
sudo ./nginx-domain-manager.sh enable example.com

# List all sites
sudo ./nginx-domain-manager.sh list

# Disable site
sudo ./nginx-domain-manager.sh disable example.com

# Delete site
sudo ./nginx-domain-manager.sh delete example.com

Monitoring Multiple Domains

# Check access logs for all domains
tail -f /var/www/*/logs/access.log

# Count requests per domain
for domain in /var/www/*; do
    echo "$(basename $domain): $(wc -l < $domain/logs/access.log) requests"
done

# Check SSL certificate expiration for all domains
for domain in /etc/letsencrypt/live/*/; do
    domain=$(basename $domain)
    echo "$domain:"
    echo | openssl s_client -servername $domain -connect $domain:443 2>/dev/null | openssl x509 -noout -dates
done

Best Practices

  1. Use separate configuration files: One file per domain
  2. Organize with sites-available/enabled: Easy management
  3. Separate log files: Per-domain logging
  4. Use snippets: Share common configurations
  5. Set up default server: Handle undefined domains
  6. Use SSL for all domains: Let's Encrypt is free
  7. Regular backups: Configuration and content
  8. Monitor resources: Check limits per domain
  9. Document configurations: Comment complex setups
  10. Test thoroughly: Use nginx -t before reloading

Troubleshooting

Issue: Domain shows wrong site

# Check server_name configuration
sudo nginx -T | grep -A 10 "server_name domain.com"

# Check DNS resolution
nslookup domain.com

# Test with explicit Host header
curl -H "Host: domain.com" http://your-server-ip

Issue: SSL certificate error

# Check certificate paths
sudo nginx -T | grep ssl_certificate

# Verify certificate files exist
ls -la /etc/letsencrypt/live/domain.com/

# Renew certificates
sudo certbot renew

# Test SSL
openssl s_client -connect domain.com:443 -servername domain.com

Issue: 404 errors

# Check root directive
sudo nginx -T | grep -B 5 "root /var/www"

# Verify files exist
ls -la /var/www/domain.com/public/

# Check permissions
namei -l /var/www/domain.com/public/index.html

22: Nginx Security Best Practices and Hardening

Security Fundamentals

A secure Nginx configuration protects against:

  • DDoS attacks: Resource exhaustion
  • SQL injection: Through application proxying
  • XSS attacks: Cross-site scripting
  • Information disclosure: Server details
  • Unauthorized access: Protected resources
  • Man-in-the-middle: Unencrypted connections

1. Hide Nginx Version

http {
    # Hide Nginx version in headers and error pages
    server_tokens off;
}

Test:

curl -I http://example.com
# Should NOT show: Server: nginx/1.18.0
# Should show: Server: nginx

2. Disable Unnecessary HTTP Methods

server {
    listen 80;
    server_name example.com;

    # Allow only specific methods
    if ($request_method !~ ^(GET|POST|HEAD)$) {
        return 405;
    }

    location / {
        # Or use limit_except
        limit_except GET POST HEAD {
            deny all;
        }

        try_files $uri $uri/ =404;
    }
}

3. Security Headers

server {
    listen 443 ssl http2;
    server_name example.com;

    # Prevent clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Prevent MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Enable XSS protection
    add_header X-XSS-Protection "1; mode=block" always;

    # Referrer Policy
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # HSTS (HTTP Strict Transport Security)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;

    # Permissions Policy (formerly Feature Policy)
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}

4. SSL/TLS Configuration

server {
    listen 443 ssl http2;
    server_name example.com;

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

    # Modern protocols only
    ssl_protocols TLSv1.2 TLSv1.3;

    # Strong ciphers
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # Session settings
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Diffie-Hellman parameter
    ssl_dhparam /etc/nginx/dhparam.pem;
}

Generate DH parameters:

sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096

5. Deny Access to Hidden Files

server {
    listen 80;
    server_name example.com;

    # Deny access to hidden files (.htaccess, .git, .env, etc.)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Specifically deny common files
    location ~ /\.(?:git|svn|hg|bzr|cvs) {
        deny all;
    }

    location ~ /\.(?:htaccess|htpasswd|env) {
        deny all;
    }
}

6. Protect Sensitive Directories

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;

    # Deny access to backup files
    location ~ ~$ {
        deny all;
    }

    # Deny access to config files
    location ~* \.(conf|config|ini|log)$ {
        deny all;
    }

    # Protect admin area
    location /admin {
        allow 192.168.1.0/24;
        allow 10.0.0.0/8;
        deny all;

        auth_basic "Admin Area";
        auth_basic_user_file /etc/nginx/.htpasswd;

        try_files $uri $uri/ =404;
    }
}

7. Rate Limiting

# Define rate limit zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conn:10m;

server {
    listen 80;
    server_name example.com;

    # Connection limit
    limit_conn conn 10;

    # General rate limit
    location / {
        limit_req zone=general burst=20 nodelay;
        limit_req_status 429;
    }

    # Stricter for login
    location /login {
        limit_req zone=login burst=5;
        limit_req_status 429;
    }
}

8. Request Size Limits

http {
    # Global limits
    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    server {
        listen 80;
        server_name example.com;

        # Override for specific location
        location /upload {
            client_max_body_size 100m;
        }
    }
}

9. Timeout Configuration

http {
    # Prevent slowloris attacks
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 30s;
    send_timeout 10s;

    server {
        listen 80;
        server_name example.com;
    }
}

10. Block User Agents

# Block bad bots
map $http_user_agent $bad_bot {
    default 0;
    ~*^$ 1;  # Empty user agent
    ~*(bot|crawler|spider|scraper|wget|curl) 1;
    ~*(nikto|sqlmap|nmap|masscan) 1;
}

server {
    listen 80;
    server_name example.com;

    if ($bad_bot) {
        return 403;
    }
}

11. Geographic Restrictions (GeoIP)

# Install GeoIP
sudo apt install nginx-module-geoip geoip-database
# Load module
load_module modules/ngx_http_geoip_module.so;

http {
    geoip_country /usr/share/GeoIP/GeoIP.dat;

    # Block specific countries
    map $geoip_country_code $blocked_country {
        default no;
        CN yes;  # China
        RU yes;  # Russia
        KP yes;  # North Korea
    }

    server {
        listen 80;
        server_name example.com;

        if ($blocked_country = yes) {
            return 403 "Access from your country is not allowed";
        }
    }
}

12. Prevent Hotlinking

server {
    listen 80;
    server_name example.com;

    # Prevent image hotlinking
    location ~* \.(jpg|jpeg|png|gif)$ {
        valid_referers none blocked example.com *.example.com;

        if ($invalid_referer) {
            return 403;
            # Or return replacement image:
            # rewrite ^ /images/hotlink-placeholder.jpg;
        }
    }
}

13. ModSecurity WAF (Web Application Firewall)

# Install ModSecurity
sudo apt install nginx-module-modsecurity libmodsecurity3

# Clone OWASP Core Rule Set
cd /opt
sudo git clone https://github.com/coreruleset/coreruleset
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.conf
# Load module
load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsecurity.conf;

    server {
        listen 80;
        server_name example.com;

        # Your configuration
    }
}

14. Fail2Ban Integration

# Install Fail2Ban
sudo apt install fail2ban

Create Nginx filter:

sudo nano /etc/fail2ban/filter.d/nginx-req-limit.conf

[Definition]
failregex = limiting requests, excess:.* by zone.*client: <HOST>
ignoreregex =

Create jail:

sudo nano /etc/fail2ban/jail.local

[nginx-req-limit]
enabled = true
filter = nginx-req-limit
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
findtime = 600
bantime = 7200
maxretry = 10

[nginx-auth]
enabled = true
filter = nginx-auth
action = iptables-multiport[name=Auth, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
maxretry = 5

Start Fail2Ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status

15. Disable Directory Listing

server {
    listen 80;
    server_name example.com;

    # Disable directory listing
    autoindex off;

    location / {
        try_files $uri $uri/ =404;
    }
}

16. Secure Cookies

# For proxy backends
location / {
    proxy_pass http://backend;

    # Add secure flags to cookies
    proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=Strict";
}

# Or modify Set-Cookie header
map $upstream_http_set_cookie $new_cookie {
    ~*^(.*?)$ "$1; HTTPOnly; Secure; SameSite=Strict";
}

server {
    location / {
        proxy_pass http://backend;
        proxy_hide_header Set-Cookie;
        add_header Set-Cookie $new_cookie;
    }
}

17. X-Forwarded Headers Security

server {
    listen 80;
    server_name example.com;

    # Clear potentially malicious headers
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;

    # Don't trust client-provided X-Real-IP
    proxy_set_header X-Real-IP $remote_addr;

    location / {
        proxy_pass http://backend;
    }
}

18. File Upload Security

server {
    listen 80;
    server_name example.com;

    location /upload {
        # Limit upload size
        client_max_body_size 10m;

        # Only allow specific methods
        limit_except POST {
            deny all;
        }

        # Prevent script execution in upload directory
        location ~* \.(php|php5|phtml|pl|py|jsp|asp|sh|cgi)$ {
            deny all;
        }

        proxy_pass http://backend;
    }
}

19. Complete Secure Configuration

# Security headers snippet
# /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" 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 "geolocation=(), microphone=(), camera=()" always;

# SSL configuration snippet
# /etc/nginx/snippets/ssl-secure.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_dhparam /etc/nginx/dhparam.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=strict:10m rate=2r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;

# Bad bot blocking
map $http_user_agent $bad_bot {
    default 0;
    ~*^$ 1;
    ~*(bot|crawler|spider|scraper) 1;
}

# Main configuration
http {
    # Hide version
    server_tokens off;

    # Buffer settings
    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;

    # Timeouts
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 30s;
    send_timeout 10s;

    # Disable directory listing
    autoindex off;

    server {
        listen 80;
        server_name example.com;
        return 301 https://example.com$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name example.com;

        root /var/www/example.com;
        index index.html;

        # SSL
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
        include snippets/ssl-secure.conf;

        # Security headers
        include snippets/security-headers.conf;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

        # Rate limiting
        limit_conn conn 10;
        limit_req zone=general burst=20 nodelay;

        # Block bad bots
        if ($bad_bot) {
            return 403;
        }

        # Block unwanted methods
        if ($request_method !~ ^(GET|POST|HEAD)$) {
            return 405;
        }

        # Deny hidden files
        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }

        # Deny backup files
        location ~ ~$ {
            deny all;
        }

        # Main location
        location / {
            try_files $uri $uri/ =404;
        }

        # Prevent hotlinking
        location ~* \.(jpg|jpeg|png|gif)$ {
            valid_referers none blocked example.com *.example.com;
            if ($invalid_referer) {
                return 403;
            }
            expires 30d;
        }

        # Admin area protection
        location /admin {
            allow 192.168.1.0/24;
            deny all;

            auth_basic "Admin Area";
            auth_basic_user_file /etc/nginx/.htpasswd;

            limit_req zone=strict burst=5;
        }
    }
}

20. Security Auditing

Test SSL Configuration

# Using testssl.sh
git clone https://github.com/drwetter/testssl.sh.git
cd testssl.sh
./testssl.sh https://example.com

# Online tool
https://www.ssllabs.com/ssltest/analyze.html?d=example.com

Test Security Headers

# Check headers
curl -I https://example.com

# Use securityheaders.com
https://securityheaders.com/?q=example.com

# Use Mozilla Observatory
https://observatory.mozilla.org/analyze/example.com

Scan for Vulnerabilities

# Using Nikto
nikto -h https://example.com

# Using OWASP ZAP
zap-cli quick-scan https://example.com

# Using Nmap
nmap -sV --script=http-security-headers example.com

Security Checklist

  • Hide Nginx version (server_tokens off)
  • Use HTTPS only (redirect HTTP to HTTPS)
  • Configure strong SSL/TLS (TLS 1.2+)
  • Generate DH parameters (4096-bit)
  • Enable HSTS
  • Implement security headers
  • Rate limiting configured
  • Connection limits set
  • Request size limits defined
  • Timeout configuration
  • Deny hidden files access
  • Disable directory listing
  • Block bad user agents
  • Prevent hotlinking
  • Protect admin areas
  • Implement Fail2Ban
  • Regular security audits
  • Keep Nginx updated
  • Monitor logs for suspicious activity
  • Regular backups

Best Practices

  1. Keep Nginx updated: sudo apt update && sudo apt upgrade nginx
  2. Use minimal modules: Compile only needed modules
  3. Run as non-root user: Use www-data or nginx user
  4. Separate processes: Run Nginx and applications separately
  5. Monitor logs: Check for suspicious activity
  6. Regular audits: Test security configuration
  7. Implement WAF: Use ModSecurity
  8. Use fail2ban: Auto-ban malicious IPs
  9. Backup regularly: Configuration and content
  10. Document changes: Maintain change log