SecHead
Scan a siteContact Us
Platform Guide8 min read

Security Headers for Nginx: Complete nginx.conf Configuration Guide

Add all essential HTTP security headers to Nginx in minutes. Copy-paste nginx.conf snippets for CSP, HSTS, X-Frame-Options, Referrer-Policy, and Permissions-Policy - with explanations for each directive.

SL
Seven Labs · 1 July 2026
1,517 words

Security Headers for Nginx: Complete nginx.conf Configuration Guide

Nginx ships with zero security headers configured by default. A freshly installed Nginx server will advertise its version number, serve pages without clickjacking protection, and give browsers no instruction on how to handle mixed content or MIME type ambiguity. Fixing this takes less than ten minutes - and the difference between a default Nginx server and a properly configured one is the difference between an F grade and an A.

This guide provides complete, production-ready nginx.conf snippets for every important security header. Scan your site on SecHead to see your current grade before applying changes, then re-scan afterward to verify.


How Nginx Handles Response Headers

Nginx adds headers using the add_header directive, placed inside server {} or location {} blocks. One important nuance: by default, add_header directives in a child block (like location /) override any add_header directives in the parent server {} block - they don't merge. This is the most common cause of missing headers in Nginx setups.

The safest pattern is to define all security headers in a dedicated snippet file and include it in every relevant block.


The Complete Security Headers Snippet

Create /etc/nginx/snippets/security-headers.conf

nginx
# -------------------------------------------------------
# Security Headers - SecHead recommended configuration
# Nginx - include this file in server {} blocks
# -------------------------------------------------------

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

# Clickjacking protection
add_header X-Frame-Options "DENY" always;

# HTTPS enforcement (set only on HTTPS server blocks)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

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

# Restrict browser feature access
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

# Content Security Policy (adjust to match your site's actual dependencies)
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;

The always parameter ensures headers are sent even on error responses (4xx, 5xx). Without it, headers are only sent with 2xx responses.


Including the Snippet in Your Server Block

nginx
# /etc/nginx/sites-available/example.com

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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

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

    # Security headers
    include snippets/security-headers.conf;

    # Remove Nginx version from Server header
    server_tokens off;

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

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

Header-by-Header Breakdown

Strict-Transport-Security (HSTS)

HSTS instructs browsers to only connect to your domain over HTTPS, even if the user types http:// or clicks an HTTP link. The browser enforces this locally - no round-trip needed.

nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
  • max-age=31536000 - remember this rule for one year
  • includeSubDomains - applies to all subdomains (only add if all your subdomains support HTTPS)
  • preload - opt into being hardcoded into browsers' built-in HSTS list (submit at hstspreload.org after confirming your site is fully HTTPS)

Only add HSTS to your listen 443 server block, never the port 80 block. See the full HSTS guide for details on the preload list and rollout strategy.


Content-Security-Policy

CSP controls where the browser is allowed to load scripts, styles, images, fonts, and other resources from. It's the most powerful security header and the one that requires the most customisation.

The baseline policy in the snippet above (default-src 'none') blocks everything, then allows only what you explicitly list. You'll need to expand it:

If you load Google Fonts:

nginx
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;

If you use inline scripts (via a backend that generates nonces):

nginx
# Set this header dynamically in your application layer, not in nginx.conf,
# since the nonce value must change on every request.
# Use nginx's proxy_hide_header + add_header pattern if you're proxying to Node/PHP.

For a deep dive into every CSP directive and how to write a policy that won't get flagged in a pentest, see the Content Security Policy explainer and the pentest CSP remediation guide.


X-Frame-Options

Prevents your pages from being embedded in <iframe> elements on other sites, blocking clickjacking attacks where an attacker overlays your interface with an invisible frame.

nginx
add_header X-Frame-Options "DENY" always;

Use SAMEORIGIN if your application uses iframes within the same domain (e.g., a self-hosted embedded widget). ALLOW-FROM uri was deprecated and is no longer supported in modern browsers - use CSP frame-ancestors instead if you need domain-specific allowlisting.


X-Content-Type-Options

Prevents browsers from guessing (sniffing) the MIME type of a response. Without this, a browser might render a text file that contains HTML as an actual HTML page - a common path to XSS via malicious file uploads.

nginx
add_header X-Content-Type-Options "nosniff" always;

Zero compatibility risk, zero downside. Add it to every Nginx configuration unconditionally.


Referrer-Policy

Controls what URL information is included in the Referer header when users navigate away from your site. Without this, clicking an external link sends the full URL of your page (including any query string parameters like ?token=... or ?email=...) to the destination site.

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

This sends the full URL for same-origin navigations (your own analytics can still use it) but only the domain for cross-origin navigations. For stricter privacy, use no-referrer or same-origin.


Permissions-Policy

Restricts which powerful browser APIs this page is permitted to access. Primarily useful for limiting what third-party scripts (analytics, ads, embeds) can access on your users' devices.

nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;

Each empty () means "blocked for all origins including the site itself." Adjust any you actively use to (self) to allow just your own domain.


Removing Server Fingerprinting

By default, Nginx sends Server: nginx/1.24.0 in every response. This tells attackers exactly which version to look up in CVE databases.

nginx
# In http {} block in nginx.conf
http {
    server_tokens off;
    # ...
}

server_tokens off reduces the header to just Server: nginx with no version. To remove the Server header entirely (requires the ngx_http_headers_more_module):

nginx
more_clear_headers Server;

Nginx + Reverse Proxy Setup (Node.js, PHP-FPM)

If Nginx is proxying to an application server, the application may set its own headers. To avoid duplicate headers, use proxy_hide_header to suppress upstream headers you want to override from Nginx:

nginx
location / {
    proxy_pass http://localhost:3000;
    proxy_hide_header X-Powered-By;

    include snippets/security-headers.conf;
}

Important: As mentioned earlier, add_header in a location {} block overrides parent server {} block headers. Always either use the include pattern in every location block, or use the ngx_http_headers_more_module which has a true "always append" behavior.


Testing Your Configuration

Test config syntax before reloading

bash
sudo nginx -t

If you get syntax is ok and test is successful, proceed.

Reload Nginx (zero-downtime)

bash
sudo nginx -s reload
# or
sudo systemctl reload nginx

Verify headers from the command line

bash
curl -sI https://example.com | grep -i -E "strict|x-frame|x-content|referrer|permissions|content-security"

Get your full grade

Run a SecHead scan on your domain to see your grade and confirm every header is configured correctly. A properly set up Nginx server should score A or A+.


Common Mistakes

Duplicate headers

If your application (Node.js, PHP) sets the same headers that Nginx also sets, browsers may receive two copies - and some browsers handle this unpredictably for CSP. Use proxy_hide_header to suppress the upstream header if Nginx is adding it.

add_header inheritance

Any location {} block that has its own add_header directive will silently drop all headers from the parent server {} block. Always use the snippet include pattern or duplicate the headers in each location block.

HSTS on port 80

Never set Strict-Transport-Security in a listen 80 server block. If a user hits port 80 and receives HSTS, browsers will refuse to connect over HTTP - but the connection is already unencrypted when they receive the header.


Full Production Configuration Reference

nginx
# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
nginx
# /etc/nginx/nginx.conf (http block)
http {
    server_tokens off;
    # ...
}
nginx
# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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

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

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    include snippets/security-headers.conf;

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

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

After configuring your Nginx server, scan your domain on SecHead to get your grade and see exactly which headers are present. For the equivalent configuration on other platforms, see the Apache security headers guide and the Next.js security headers guide. The complete security headers checklist covers every header and why each one matters.

Related articles

Free tool

Check your own security headers

Instant grade, plain-language explanations, and a full remediation plan - no signup needed.

Scan your site now →