IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

Configure Nginx Security Headers on Ubuntu VPS

By Raman Kumar

Share:

Updated on Jul 2, 2026

Configure Nginx Security Headers on Ubuntu VPS

Why Security Headers Matter for Your Hosting Setup

A freshly provisioned Ubuntu VPS running Nginx will serve pages just fine — but without HTTP security headers, every browser visiting your site is operating without basic protections. Headers like X-Frame-Options, Content-Security-Policy, and Strict-Transport-Security take seconds to add and can stop a meaningful class of client-side attacks before they reach your application code.

This tutorial walks you through adding Nginx security headers on an Ubuntu VPS step by step. It assumes you have Nginx installed and at least one active server block. If you're running Ubuntu 22.04 or 24.04 LTS on a Hostperl VPS, the paths and commands here will match exactly.

What You'll Be Configuring

Before touching any files, it helps to know what each header actually does. Here's a quick reference:

  • Strict-Transport-Security (HSTS) — Forces browsers to use HTTPS for a set period, even if a user types http://.
  • X-Frame-Options — Prevents your site from being embedded in an iframe on another domain, blocking clickjacking.
  • X-Content-Type-Options — Stops browsers from guessing MIME types, which can prevent certain script injection scenarios.
  • Referrer-Policy — Controls how much referrer information is sent when users click links off your site.
  • Permissions-Policy — Restricts which browser APIs (camera, microphone, geolocation) a page can request.
  • Content-Security-Policy (CSP) — The most powerful header. Defines which sources the browser may load scripts, styles, and fonts from.

You don't need all of them on day one. A solid baseline covers the first five. CSP gets its own section because it requires more thought for most real applications.

Step 1 — Locate Your Nginx Server Block

On Ubuntu, Nginx site configurations live in /etc/nginx/sites-available/. Your active sites are symlinked from /etc/nginx/sites-enabled/.

List what's enabled:

ls -la /etc/nginx/sites-enabled/

Open the config for the site you want to harden. Replace yourdomain.conf with your actual filename:

sudo nano /etc/nginx/sites-available/yourdomain.conf

You'll see a server { } block. That's where the headers go — but rather than adding them directly there, a cleaner approach is to put them in a shared snippet file. This way, every site on the server can include the same headers without duplicating lines.

Step 2 — Create a Security Headers Snippet

Create a new file at /etc/nginx/snippets/security-headers.conf:

sudo nano /etc/nginx/snippets/security-headers.conf

Paste the following baseline configuration:

# HSTS — tell browsers to use HTTPS for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

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

# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

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

# Remove server version from response headers
server_tokens off;

Save and close the file (Ctrl+O, Enter, Ctrl+X in nano).

The always flag at the end of each directive ensures the header is sent on all response codes — including 4xx and 5xx — not just on 200 OK responses. Without it, a 404 page leaks your header configuration gaps to scanners.

Step 3 — Include the Snippet in Your Server Block

Open your site's Nginx config again:

sudo nano /etc/nginx/sites-available/yourdomain.conf

Inside the server { } block for your HTTPS (port 443) listener, add this line:

include snippets/security-headers.conf;

A minimal server block should look something like this after the edit:

server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

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

    include snippets/security-headers.conf;

    root /var/www/yourdomain/html;
    index index.html index.php;

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

If you're managing multiple domains on the same VPS, just add the include line to each relevant server block. You only maintain one file. If you want to learn how multi-domain SSL certificates work alongside this setup, the multi-domain SSL setup with Certbot guide covers that in detail.

Step 4 — Test and Reload Nginx

Always test before reloading. A syntax error in a config file takes down every site on the server.

sudo nginx -t

You should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Then reload without dropping existing connections:

sudo systemctl reload nginx

Not restart. Reload applies the new configuration gracefully. Restart drops all active connections, which matters if you have long-lived requests or WebSocket connections.

Step 5 — Verify the Headers Are Being Sent

Use curl to check the response headers from the command line:

curl -I https://yourdomain.com

Look for lines like these in the output:

strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()

If any are missing, double-check that the include line is inside the correct server { } block and that you reloaded (not just saved) the config.

You can also paste your domain into securityheaders.com for a scored report. A baseline config like the one above should return an A rating without any CSP directives yet.

Adding a Content-Security-Policy Without Breaking Your Site

CSP is the header most people either skip entirely or add incorrectly — then wonder why their fonts or analytics scripts stop loading. The trick is to start in report-only mode.

Add this line to your snippet temporarily:

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; report-uri /csp-report" always;

This sends the CSP header to browsers but does not enforce it — violations are logged, not blocked. Watch your browser's developer console under the Network tab for CSP violations for a day or two while navigating your own site. Each violation tells you exactly which source you need to whitelist.

Once you've collected enough data, switch from Content-Security-Policy-Report-Only to Content-Security-Policy with your adjusted directives. For a WordPress site, you'll almost certainly need to add 'unsafe-inline' for scripts (or use nonces) and whitelist your CDN domain if you use one.

This iterative approach saves you from a common support headache: a strict CSP that silently breaks checkout forms, admin panels, or third-party widgets hours after deployment. If you've recently migrated to a VPS and want to cross-check your broader server security posture, the VPS security checklist for hosting customers covers complementary hardening steps.

Handling the HSTS Preload List

The HSTS header in this guide uses max-age=31536000; includeSubDomains. That's a solid production value — one year, applying to all subdomains.

You may have seen a third directive: preload. Adding preload tells browsers to include your domain in a hardcoded list shipped with Chrome, Firefox, and others. Once on the list, your domain cannot be visited over HTTP even on a fresh browser install.

Only add preload if you are certain every subdomain on your domain runs HTTPS. Removing a domain from the preload list takes months. For most hosting customers, the two-directive version is the right call until your infrastructure is stable.

Protecting Admin Paths with Extra Header Restrictions

If your site has an admin or login path (e.g., /wp-admin, /admin, /phpmyadmin), you can apply stricter headers to that location block specifically.

Inside your server block, after the global include:

location /wp-admin {
    add_header X-Frame-Options "DENY" always;
    add_header Cache-Control "no-store, no-cache, must-revalidate" always;
    try_files $uri $uri/ /index.php?$args;
}

Note: Nginx does not inherit add_header directives from parent blocks when you define add_header inside a child location block. If you add headers in a location block, you must re-include the full set there too, or use the snippet include again. This is a common configuration mistake — worth reading through the multi-site virtual host setup guide if you're managing several sites and want to understand how inheritance works across different web servers.

Keeping Logs Tidy After Changes

Once security headers are in place, your Nginx error and access logs may start catching traffic you weren't seeing before — particularly bots probing for missing headers. Log files can grow quickly. If you haven't set up log rotation yet, the Logrotate setup guide for Ubuntu VPS is worth running through next. Unmanaged logs will eventually fill your disk, which causes its own class of hosting problems.

Running Nginx on a VPS with real traffic? Hostperl's managed VPS hosting gives you full root access, SSD storage, and NZ-based support that actually picks up. If you're growing past shared hosting limits, our dedicated servers are a natural next step — with the same hands-on support team behind them.

Frequently Asked Questions

Do security headers affect site speed?

No measurable impact. Headers add a negligible number of bytes to each HTTP response. HSTS can actually improve perceived performance by eliminating HTTP-to-HTTPS redirects on repeat visits.

Will these headers break my WordPress site?

The baseline five headers (HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) are safe for WordPress without any adjustment. CSP requires more careful tuning because WordPress core and many plugins load inline scripts.

My site uses an iframe embed — will X-Frame-Options break it?

If you legitimately embed content in an iframe from the same domain, SAMEORIGIN is fine. If you embed from a different domain (e.g., a booking widget), you may need to set a specific frame-ancestors directive inside a CSP header instead, and remove X-Frame-Options. Modern browsers prefer CSP frame-ancestors over the older header.

Can I use these headers on HTTP (non-SSL) sites?

Skip HSTS on HTTP server blocks — it only applies to HTTPS connections and has no effect on plain HTTP. The other headers can be applied, but running production hosting without SSL in 2026 is not advisable for any customer-facing site.

How often should I review my security header configuration?

After any major application update, after adding new third-party scripts or CDN domains, and at least once per year as a routine check. CSP in particular may need updating when your application changes which external resources it loads.