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:
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:
- Exact name
- Longest wildcard starting with asterisk (*.example.com)
- Longest wildcard ending with asterisk (mail.*)
- First matching regular expression
- 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:
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;
}
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
- Use separate configuration files: One file per domain
- Organize with sites-available/enabled: Easy management
- Separate log files: Per-domain logging
- Use snippets: Share common configurations
- Set up default server: Handle undefined domains
- Use SSL for all domains: Let's Encrypt is free
- Regular backups: Configuration and content
- Monitor resources: Check limits per domain
- Document configurations: Comment complex setups
- Test thoroughly: Use
nginx -tbefore 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¶
Test:
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:
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)¶
# 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¶
Create Nginx filter:
Create jail:
[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:
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
- Keep Nginx updated:
sudo apt update && sudo apt upgrade nginx - Use minimal modules: Compile only needed modules
- Run as non-root user: Use www-data or nginx user
- Separate processes: Run Nginx and applications separately
- Monitor logs: Check for suspicious activity
- Regular audits: Test security configuration
- Implement WAF: Use ModSecurity
- Use fail2ban: Auto-ban malicious IPs
- Backup regularly: Configuration and content
- Document changes: Maintain change log