/ Security Audit Fixes / nginx

How to Fix Security Headers in Bare nginx

Bare nginx — without Plesk, cPanel or any control panel — gives you full control over every header, every cipher, every protocol. This guide walks through every fix our Security Audit raises on a directly-configured nginx server: adding the six recommended HTTP security headers, configuring modern TLS protocols and publishing security.txt. Tested against nginx 1.24 on Ubuntu 22.04 / 24.04 LTS. For Apache, see the Apache variant; for the full finding catalogue, see Security Audit Fixes.

1. Add the six HTTP security headers

The six headers our audit checks (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) all go inside your server block via add_header. The critical nginx quirk: add_header at parent scope is silently replaced when any add_header appears in an inner block, so put all six in the same location to avoid surprises.

Step 1
Locate your vhost config
The exact path depends on your distribution. Common locations:
/etc/nginx/sites-available/yourdomain.conf      # Debian/Ubuntu convention
/etc/nginx/conf.d/yourdomain.conf               # CentOS/RHEL/Amazon Linux
/etc/nginx/nginx.conf                            # Single-file deployments
Open the file with your preferred editor:
sudo nano /etc/nginx/sites-available/yourdomain.conf
Step 2
Add the six directives to your server block
Inside the server { ... } block that handles HTTPS (port 443), paste the following six directives. Always include always so the headers apply to error responses too:
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 Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'" always;
⚠️ The Content-Security-Policy is a sensible default but may block legitimate third-party scripts (analytics, embedded video, advertising). Test in a staging environment first. Start with Google's CSP Evaluator if you need to loosen it.
Step 3
Test and reload
Always test config before reloading:
sudo nginx -t
If the test passes, reload:
sudo nginx -s reload
Or via systemd:
sudo systemctl reload nginx
Step 4
Verify the headers are present
Test with curl from outside the server:
curl -sI https://yourdomain.com/ | grep -iE "strict-transport|x-frame|x-content|referrer|permissions|content-security"
All six headers should appear in the output. If any are missing, check that the add_header directives are inside the right server block and that you reloaded (not just edited) nginx.
💡 Reusing security headers across many domains? Put them in a single file like /etc/nginx/snippets/security-headers.conf and include snippets/security-headers.conf; inside each server block. One source of truth, easy to update.

2. Configure modern TLS protocols

The audit also flags TLS 1.0 and TLS 1.1 — both deprecated, both insecure. nginx defaults vary by distribution, so do not assume your server is already on TLS 1.2+.

Step 1
Set protocols and ciphers
In the same server block (or globally in nginx.conf http block):
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_prefer_server_ciphers off is correct for modern TLS — clients now pick better ciphers than servers do.
Step 2
Verify with SSL Labs
Run SSL Labs SSL Test against your domain. Target grade: A or A+. A+ requires HSTS preload, which you already added in step 1.

3. Publish security.txt at /.well-known/security.txt

RFC 9116 standardises the /.well-known/security.txt file so security researchers know where to report vulnerabilities. Browsers and audit tools both check for it.

Step 1
Create the file in your document root
Make the directory if it does not exist:
sudo mkdir -p /var/www/yourdomain/public/.well-known
Create security.txt with at minimum Contact and Expires:
Contact: mailto:security@yourdomain.com
Expires: 2027-05-18T00:00:00.000Z
Preferred-Languages: en
Canonical: https://yourdomain.com/.well-known/security.txt
Adjust the email and expiry date to suit. Expiry must be a future date — past expiry invalidates the file.
Step 2
Add a location block for correct Content-Type
nginx may serve .txt with the wrong MIME type by default. Force text/plain:
location = /.well-known/security.txt {
    default_type text/plain;
}
The = makes it an exact match so it does not interfere with other .well-known paths (such as ACME challenges from Let's Encrypt).
Step 3
Reload and verify
sudo nginx -t && sudo nginx -s reload
curl -sI https://yourdomain.com/.well-known/security.txt
Expected: HTTP/2 200 and content-type: text/plain.

4. Re-run the audit

After all three sections above are complete, run the Security Audit against your domain. All six headers should pass, TLS protocols should be modern-only, and the security.txt check should show green. Most bare-nginx servers reach 95-100/100 after this guide.

💡 If you manage 10+ sites on bare nginx, build the security headers into a shared snippet and source it everywhere. A single change rolls out site-wide via nginx -s reload.

🛡 Run the audit after fixing

Verify every header, TLS protocol and security.txt with a fresh scan.

Run Security Audit →
Related Guides: Security Audit Fixes  ·  Fix in Apache  ·  Fix via Cloudflare  ·  Security Audit Guide
💬 Got a problem?