Harden your web server with security headers, SSL best practices, directory protection, rate limiting, and ModSecurity.
5 min read
A web server with default settings is functional but not secure. This guide covers the practical hardening steps for Nginx and Apache (including LiteSpeed, which uses Apache-compatible configs) on a GoZen VPS. For platform-level protections, see GoZen Security.
Security headers tell browsers how to handle your content. These protect against cross-site scripting, clickjacking, and data injection attacks.
Add to your server block or a shared snippets/security-headers.conf:
# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Block page embedding (prevents clickjacking)
add_header X-Frame-Options "SAMEORIGIN" always;
# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Restrict browser features
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Force HTTPS for 1 year (only add after confirming SSL works)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
To share headers across all sites, create a snippet:
sudo nano /etc/nginx/snippets/security-headers.conf
# Paste the headers above
# Then include in each server block:
# include snippets/security-headers.conf;
Enable the headers module:
sudo a2enmod headers
Add to your virtual host or .htaccess:
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off
ModSecurity for Nginx requires compiling the Nginx connector module. For most users, a simpler approach is to add Cloudflare’s free WAF in front of your server. See How to Set Up Cloudflare.
If you need a local WAF, consider running Nginx with the OWASP CoreRuleSet via Docker.
Include the rules. The stock security2.conf on Debian/Ubuntu already loads /etc/modsecurity/*.conf and the packaged rule set (/usr/share/modsecurity-crs/*.load). Loading either twice gives “Found another rule with the same id” and Apache won’t start, so replace the packaged CRS line instead of adding to the file:
If you’d rather use the packaged rule set (sudo apt install modsecurity-crs), skip the git clone step and leave security2.conf as it is.
sudo systemctl restart apache2
Watch the logs (/var/log/apache2/modsec_audit.log) for a few days. Once you’ve cleared any false positives, set SecRuleEngine On in /etc/modsecurity/modsecurity.conf and restart Apache to start blocking.