Welcome to Part 9 of our comprehensive NGINX on Ubuntu series! We’ll implement security hardening measures, access controls, and protection mechanisms to safeguard your web server against threats and attacks.
NGINX Security Fundamentals
Security hardening involves multiple layers of protection including access controls, rate limiting, security headers, and monitoring to create a robust defense against various attack vectors.
graph TD A[Security Layers] --> B[Network Level] A --> C[Application Level] A --> D[Server Level] A --> E[Content Level] B --> F[Rate LimitingIP FilteringGeoBlocking] C --> G[AuthenticationAuthorizationInput Validation] D --> H[Server HardeningSSL/TLSSecurity Headers] E --> I[Content SecurityXSS ProtectionCSRF Protection] J[Common Threats] --> K[DDoS Attacks] J --> L[Brute Force] J --> M[SQL Injection] J --> N[XSS Attacks] J --> O[Data Breaches] style A fill:#e1f5fe style J fill:#ffebee style F fill:#e8f5e8 style G fill:#e8f5e8 style H fill:#e8f5e8 style I fill:#e8f5e8
Basic Security Configuration
# Create security configuration snippet
sudo nano /etc/nginx/snippets/security-headers.conf
# Security headers configuration
# Hide NGINX version
server_tokens off;
# Security Headers
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 "camera=(), microphone=(), geolocation=()" always;
# Content Security Policy (adjust based on your needs)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self';" always;
# HSTS (HTTP Strict Transport Security) - only for HTTPS
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Remove potentially dangerous headers
more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';
# Limit request methods
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$ ) {
return 405;
}
# Block common exploit attempts
location ~* "(eval\(|javascript:|vbscript:|onload=)" {
deny all;
}
# Block access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block access to backup files
location ~* \.(bak|backup|old|orig|save|swo|swp|tmp)$ {
deny all;
access_log off;
log_not_found off;
}
Rate Limiting and DDoS Protection
# Configure rate limiting in nginx.conf
sudo nano /etc/nginx/nginx.conf
# Add to http block in nginx.conf
http {
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=downloads:10m rate=5r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_conn_zone $server_name zone=perserver:10m;
# Request size limits
client_max_body_size 64m;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
# Timeout settings for security
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
# Buffer overflow protection
client_body_buffer_size 1K;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;
}
Rate Limited Virtual Host
# Create secure virtual host with rate limiting
sudo nano /etc/nginx/sites-available/secure.example.com
server {
listen 80;
server_name secure.example.com;
root /var/www/secure/public_html;
index index.html index.php;
# Include security headers
include /etc/nginx/snippets/security-headers.conf;
# Connection limits
limit_conn conn_limit 10;
limit_conn perserver 100;
# General rate limiting
limit_req zone=general burst=20 nodelay;
limit_req_status 429;
# Main location
location / {
try_files $uri $uri/ =404;
# Additional security for root
location = / {
limit_req zone=general burst=5 nodelay;
}
}
# Login/Admin areas with strict rate limiting
location ~ ^/(admin|login|wp-login|wp-admin) {
limit_req zone=login burst=3 nodelay;
# IP whitelist for admin (optional)
# allow 192.168.1.0/24;
# allow 203.0.113.0/24;
# deny all;
try_files $uri $uri/ =404;
}
# API endpoints
location /api/ {
limit_req zone=api burst=50 nodelay;
# API key validation (implement as needed)
# if ($http_x_api_key = "") {
# return 401 "API key required";
# }
proxy_pass http://backend_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# File downloads with rate limiting
location /downloads/ {
limit_req zone=downloads burst=2 nodelay;
limit_rate 1m; # Limit download speed to 1MB/s
# Log downloads
access_log /var/log/nginx/downloads.log;
}
# PHP processing with security
location ~ \.php$ {
limit_req zone=general burst=10 nodelay;
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Security parameters
fastcgi_param PHP_ADMIN_VALUE "open_basedir=$document_root:/tmp/";
fastcgi_param PHP_VALUE "upload_max_filesize=64M";
fastcgi_param PHP_VALUE "post_max_size=64M";
}
# Block access to sensitive files
location ~* \.(htaccess|htpasswd|ini|log|sh|sql|conf)$ {
deny all;
}
# Prevent access to version control
location ~ /\.(git|svn|hg|bzr) {
deny all;
}
}
IP Access Control and Geo-blocking
# Create IP access control configuration
sudo nano /etc/nginx/snippets/ip-access-control.conf
# IP Access Control Configuration
# Whitelist trusted IPs
geo $trusted_ip {
default 0;
127.0.0.1 1;
192.168.1.0/24 1;
203.0.113.0/24 1; # Replace with your office IP range
}
# Blacklist known bad IPs
geo $blocked_ip {
default 0;
# Add known malicious IPs
# 198.51.100.100 1;
# 203.0.113.200 1;
}
# Map user agents to block bad bots
map $http_user_agent $blocked_agent {
default 0;
~*bot 0; # Allow legitimate bots
~*googlebot 0;
~*bingbot 0;
~*slurp 0;
# Block bad bots and scrapers
~*scanner 1;
~*scraper 1;
~*harvester 1;
~*nikto 1;
~*sqlmap 1;
~*masscan 1;
~*nmap 1;
"" 1; # Block empty user agents
}
# Country-based blocking (requires GeoIP module)
# Install: sudo apt install nginx-module-geoip
# geo $blocked_country {
# default 0;
# CN 1; # Block China
# RU 1; # Block Russia
# KP 1; # Block North Korea
# }
IP Protected Virtual Host
# Create IP protected virtual host
sudo nano /etc/nginx/sites-available/ip-protected.example.com
server {
listen 80;
server_name ip-protected.example.com;
root /var/www/protected/public_html;
# Include IP access control
include /etc/nginx/snippets/ip-access-control.conf;
include /etc/nginx/snippets/security-headers.conf;
# Block based on various criteria
if ($blocked_ip) {
return 403 "Access denied - IP blocked";
}
if ($blocked_agent) {
return 403 "Access denied - User agent blocked";
}
# if ($blocked_country) {
# return 403 "Access denied - Country blocked";
# }
# Admin area - trusted IPs only
location /admin/ {
if ($trusted_ip = 0) {
return 403 "Admin access restricted";
}
try_files $uri $uri/ =404;
}
# API with IP restrictions
location /api/ {
# Allow API access from trusted IPs
if ($trusted_ip = 0) {
return 403 "API access restricted";
}
proxy_pass http://backend_api/;
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-Trusted-IP $trusted_ip;
}
# Public area with bot protection
location / {
# Rate limiting for public access
limit_req zone=general burst=10 nodelay;
try_files $uri $uri/ =404;
}
}
Authentication and Authorization
graph TD A[Authentication Methods] --> B[Basic Auth] A --> C[JWT Tokens] A --> D[Client Certificates] A --> E[OAuth Integration] B --> F[Username/PasswordSimple Implementation] C --> G[Token ValidationStateless Auth] D --> H[Certificate ValidationHigh Security] E --> I[Third-party AuthSSO Integration] J[Authorization Levels] --> K[Public Access] J --> L[Authenticated Users] J --> M[Admin Users] J --> N[API Clients] style A fill:#e1f5fe style J fill:#e8f5e8 style F fill:#fff3e0 style G fill:#fff3e0 style H fill:#fff3e0 style I fill:#fff3e0
HTTP Basic Authentication
# Create password file for basic auth
sudo nano /etc/nginx/auth/.htpasswd
# Add users (replace username and use strong passwords)
sudo sh -c "echo -n 'admin:' >> /etc/nginx/auth/.htpasswd"
sudo sh -c "openssl passwd -apr1 >> /etc/nginx/auth/.htpasswd"
sudo sh -c "echo -n 'user1:' >> /etc/nginx/auth/.htpasswd"
sudo sh -c "openssl passwd -apr1 >> /etc/nginx/auth/.htpasswd"
# Set permissions
sudo chmod 600 /etc/nginx/auth/.htpasswd
sudo chown www-data:www-data /etc/nginx/auth/.htpasswd
# Create authenticated virtual host
sudo nano /etc/nginx/sites-available/auth.example.com
server {
listen 80;
server_name auth.example.com;
root /var/www/auth/public_html;
include /etc/nginx/snippets/security-headers.conf;
# Public area (no auth required)
location /public/ {
try_files $uri $uri/ =404;
}
# Protected area with basic auth
location /protected/ {
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
try_files $uri $uri/ =404;
}
# Admin area with stricter controls
location /admin/ {
# IP restrictions + basic auth
allow 192.168.1.0/24;
deny all;
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
# Additional rate limiting for admin
limit_req zone=login burst=3 nodelay;
try_files $uri $uri/ =404;
}
# API with token-based auth
location /api/ {
# Check for API key in header
if ($http_x_api_key = "") {
return 401 "API key required";
}
# Validate API key (implement your logic)
# You can use auth_request for external validation
proxy_pass http://backend_api/;
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-API-Key $http_x_api_key;
}
}
Fail2Ban Integration
# Install Fail2Ban
sudo apt update
sudo apt install fail2ban -y
# Create NGINX filter for Fail2Ban
sudo nano /etc/fail2ban/filter.d/nginx-limit-req.conf
# Fail2Ban filter for NGINX rate limiting
[Definition]
failregex = limiting requests, excess: \S+ by zone "\S+", client:
ignoreregex =
# Create NGINX auth failure filter
sudo nano /etc/fail2ban/filter.d/nginx-auth.conf
# Fail2Ban filter for NGINX auth failures
[Definition]
failregex = user "\S*" (password mismatch|was not found), client:
no user/password was provided for basic authentication, client:
user "\S*": password mismatch, client:
ignoreregex =
# Configure Fail2Ban jail
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# Ban time (in seconds)
bantime = 3600
findtime = 600
maxretry = 3
# Email notifications (optional)
# destemail = admin@example.com
# sender = fail2ban@example.com
# action = %(action_mwl)s
[nginx-rate-limit]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 1800
[nginx-auth]
enabled = true
port = http,https
filter = nginx-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
[nginx-noscript]
enabled = true
port = http,https
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 6
bantime = 86400
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 86400
# Start and enable Fail2Ban
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
# Check Fail2Ban status
sudo fail2ban-client status
# Check specific jail status
sudo fail2ban-client status nginx-rate-limit
# Unban an IP if needed
# sudo fail2ban-client set nginx-rate-limit unbanip 192.168.1.100
Security Monitoring and Logging
# Create security monitoring script
sudo nano /usr/local/bin/nginx-security-monitor.sh
#!/bin/bash
# NGINX Security Monitor
LOG_DIR="/var/log/nginx"
SECURITY_LOG="/var/log/nginx/security.log"
log_security_event() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] SECURITY: $1" | tee -a "$SECURITY_LOG"
}
check_suspicious_activity() {
echo "=== Security Monitoring Report ==="
echo "Generated: $(date)"
echo
# Check for 4xx errors (potential attacks)
echo "--- Recent 4xx Errors ---"
tail -1000 "$LOG_DIR/access.log" | grep " 4[0-9][0-9] " | tail -5
echo
# Check for rate limiting hits
echo "--- Rate Limiting Events ---"
grep "limiting requests" "$LOG_DIR/error.log" | tail -5
echo
# Check for auth failures
echo "--- Authentication Failures ---"
grep "password mismatch\|was not found" "$LOG_DIR/error.log" | tail -5
echo
# Check for suspicious user agents
echo "--- Suspicious User Agents ---"
tail -1000 "$LOG_DIR/access.log" | grep -i "bot\|crawler\|scanner\|nikto" | grep -v "googlebot\|bingbot" | tail -5
echo
# Top attacking IPs
echo "--- Top Error-generating IPs ---"
tail -1000 "$LOG_DIR/access.log" | grep " 4[0-9][0-9] " | awk '{print $1}' | sort | uniq -c | sort -nr | head -5
echo
# Large request attempts
echo "--- Large Request Attempts ---"
grep "413\|client intended to send too large body" "$LOG_DIR/error.log" | tail -3
}
# Run security check
check_suspicious_activity
# Make executable: sudo chmod +x /usr/local/bin/nginx-security-monitor.sh
SSL/TLS Security Enhancement
# Create enhanced SSL configuration
sudo nano /etc/nginx/snippets/ssl-security.conf
# Enhanced SSL/TLS Security Configuration
# SSL Protocols and Ciphers
ssl_protocols TLSv1.2 TLSv1.3;
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;
# SSL Session Settings
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
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;
# SSL Security Headers
add_header Strict-Transport-Security "max-age=63072000; 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;
# SSL Buffer Settings
ssl_buffer_size 4k;
Testing Security Configuration
# Enable secure sites
sudo ln -s /etc/nginx/sites-available/secure.example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/auth.example.com /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Reload NGINX
sudo systemctl reload nginx
# Test rate limiting
for i in {1..15}; do curl -H "Host: secure.example.com" http://localhost/ -w "%{http_code}\n" -o /dev/null -s; done
# Test authentication
curl -H "Host: auth.example.com" http://localhost/protected/ -w "%{http_code}\n" -o /dev/null -s
curl -u admin:password -H "Host: auth.example.com" http://localhost/protected/ -w "%{http_code}\n" -o /dev/null -s
# Test security headers
curl -I -H "Host: secure.example.com" http://localhost/
# Check Fail2Ban status
sudo fail2ban-client status
# Run security monitor
/usr/local/bin/nginx-security-monitor.sh
# Test with security scanner (if available)
# nikto -h http://secure.example.com
What’s Next?
Excellent! You’ve implemented comprehensive security hardening for your NGINX server. Your setup now includes rate limiting, access controls, authentication, monitoring, and protection against common attack vectors.
Coming up in Part 10: NGINX Logging, Monitoring, and Analytics
References
This is Part 9 of our 22-part NGINX series. Your server is now properly secured and hardened! Next, we’ll set up comprehensive monitoring and analytics. Questions about security? Share them in the comments!