X-Frame-Options
X-Frame-Options controls whether a browser allows your page to be embedded in an <iframe>, <frame>, <embed>, or <object>. This prevents clickjacking attacks where an attacker overlays your page with transparent UI elements to trick users into clicking hidden buttons.
Two values are supported:
DENY: The page cannot be framed by any origin, including the same origin. Use this for pages that should never appear in an iframe.SAMEORIGIN: The page can only be framed by pages on the same origin. Use this if you embed your own pages in iframes (e.g., admin dashboards with iframe-based panels).
X-Frame-Options: DENY
Deprecation note: X-Frame-Options is superseded by CSP's frame-ancestors directive, which is more flexible (supports multiple origins, wildcards, and scheme restrictions). However, X-Frame-Options should still be set for compatibility with older browsers that do not support CSP Level 2. When both are present, frame-ancestors takes precedence in supporting browsers.
# CSP equivalent of X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
# CSP equivalent of X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
# CSP can also allow specific origins (X-Frame-Options cannot)
Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com
X-Content-Type-Options
The only valid value is nosniff:
X-Content-Type-Options: nosniff
This header prevents browsers from MIME-sniffing a response away from the declared Content-Type. Without it, a browser might interpret a file served as text/plain as HTML or JavaScript if the content looks like code -- enabling XSS attacks through file uploads or user-generated content.
For script and style resources, nosniff causes the browser to block the resource entirely if the MIME type does not match (text/javascript for scripts, text/css for stylesheets). This prevents an attacker from serving a malicious script disguised with an innocent Content-Type.
Set this header on every response. There is no downside and no reason to omit it.
Referrer-Policy
Controls how much referrer information is included in the Referer header when navigating away from your page. The referrer can leak sensitive URLs (query parameters with tokens, internal paths) to third-party sites.
Common values, from most restrictive to least:
no-referrer: Never send the Referer header. Maximum privacy but breaks some analytics and CSRF protections that rely on referrer checking.same-origin: Send the full URL for same-origin requests; no referrer for cross-origin. Good for internal navigation tracking.strict-origin-when-cross-origin: Send the full URL for same-origin, only the origin (scheme+host+port) for cross-origin HTTPS-to-HTTPS requests, and nothing for HTTPS-to-HTTP downgrades. This is the recommended default.strict-origin: Always send only the origin (not the full path), and nothing for HTTPS-to-HTTP downgrades.no-referrer-when-downgrade: Send the full URL for same-protocol requests, nothing for HTTPS-to-HTTP. This was the old browser default.
Referrer-Policy: strict-origin-when-cross-origin
Modern browsers (Chrome 85+, Firefox 87+) already default to strict-origin-when-cross-origin, but setting it explicitly ensures consistent behavior across all browsers and clearly documents your intent.
Permissions-Policy
Formerly known as Feature-Policy, Permissions-Policy controls which browser features and APIs are available to your page and any embedded iframes. This reduces your attack surface by disabling APIs you do not use.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Each directive names a feature and specifies an allowlist of origins. An empty allowlist () disables the feature entirely. Common features to restrict:
camera: Webcam access. Disable unless your site uses video chat or photo capture.microphone: Audio input. Disable unless your site records audio.geolocation: GPS/location API. Disable unless your site needs physical location.payment: Payment Request API. Disable unless you use browser-native payment flows.interest-cohort: Disables FLoC (now deprecated in favor of Topics). Settinginterest-cohort=()was a common privacy signal.display-capture: Screen sharing. Disable unless your site uses screen capture.
You can also grant features to specific origins:
# Allow camera only for your own origin and a trusted video provider
Permissions-Policy: camera=(self "https://video.example.com"), microphone=()
Recommended baseline header set
Every production web server should send at minimum these security headers on every response:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; object-src 'none'
This baseline provides HTTPS enforcement, click-jacking prevention, MIME sniffing protection, referrer privacy, feature restriction, and a basic CSP. Adjust the CSP directives based on your application's actual resource needs.
Server configuration examples
Nginx
# /etc/nginx/snippets/security-headers.conf
# Include this in your server or location blocks
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" 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 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'" always;
server {
listen 443 ssl http2;
server_name example.com;
include snippets/security-headers.conf;
# ... rest of server config
}
The always keyword ensures headers are sent on error responses too (4xx, 5xx), not just successful ones.
Apache
# httpd.conf or .htaccess (requires mod_headers)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Header always set Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"
Traefik
# traefik dynamic configuration
http:
middlewares:
security-headers:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
contentSecurityPolicy: "default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"
Apply the middleware to your routers:
# Docker label example
- "traefik.http.routers.myapp.middlewares=security-headers@file"