Kelly Michels 6 min read DevOps

Security Headers You Forgot

CSP, X-Frame-Options, X-Content-Type-Options, and HSTS.

Most sites skip the headers that actually stop browsers from being tricked. A practical guide to the four essential security headers and why nginx is the right place to enforce them site-wide.

Content-Security-Policy (CSP)

Stops inline JavaScript, script injection, and data exfiltration:

Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com

This policy says: all resources come from the same origin by default. Scripts only from same-origin. Images from same-origin or data URIs. Styles from same-origin or Google Fonts. Any attempt to inline <script> or load scripts from attacker.com is blocked.

For sites using Vite's content hashing, you can use 'self' and 'unsafe-inline' is not needed — hashed assets are served from the same domain.

X-Frame-Options

Stops your site from being embedded in an iframe on another origin:

X-Frame-Options: SAMEORIGIN

An attacker can't do clickjacking: create a transparent iframe of your site, overlay a fake button, and trick users into clicking it. SAMEORIGIN means your pages can be framed by the same origin, but not by evil.com. Use DENY if you don't need iframe embedding at all.

X-Content-Type-Options

Stops the browser from guessing file types:

X-Content-Type-Options: nosniff

Without this, a file served as text/plain but containing JavaScript can be executed by the browser if it decides the content "looks like" JS. nosniff enforces that the MIME type sent by the server is what the browser treats the file as — no guessing.

Strict-Transport-Security (HSTS)

Forces all future connections to use HTTPS:

Strict-Transport-Security: max-age=31536000; includeSubDomains

The browser remembers this for one year. Even if an attacker tricks you into clicking an http:// link, the browser silently upgrades it to https://. Prevents downgrade attacks where an attacker intercepts traffic on an unencrypted connection.

nginx implementation

Add these headers in nginx.conf inside your server block:

server {
  listen 80;
  server_name evomedia.net www.evomedia.net;

  # Security headers
  add_header Content-Security-Policy "default-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com" always;
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

  location / {
    proxy_pass http://evomedia:3000;
  }
}

The always flag ensures headers are sent even on error responses. Since you're using nginx as a reverse proxy, these headers will be applied to all responses from your backend, whether it's a static file, an API response, or an error page.

After updating nginx.conf, hot-reload with nginx -s reload for zero downtime.

← Back to Blog