A security-header change can affect script loading, framing or HTTPS access. Start by identifying the response header and rule involved rather than changing several settings at once. HSTS, CSP, X-Frame-Options and X-Content-Type-Options have distinct purposes, and a missing or incorrect setting needs to be assessed in the context of the application.
This guide explains those four headers and provides Nginx, Apache and Express examples for a staged rollout. The starter configurations use short-lived HSTS and CSP Report-Only, with explicit reporting and nonce prerequisites. Replace the example domains and test the public responses and critical user flows before enabling enforcement.
What HTTP Security Headers Control
Security headers are instructions sent with an HTTP response. They influence HTTPS use, resource loading, whether a document can be embedded, and how the browser handles content types. Their scope depends on the header and browser support. They complement authentication, output escaping, access controls and software maintenance.
These controls can reduce particular browser-side risks, but adding them does not prove compliance or block every instance of XSS, mixed content or clickjacking. Verify that each policy expresses the intended behavior and that the application can operate within it. A scanner grade is one observation, not a complete security assessment.
Verify HTTPS and certificate renewal before deploying HSTS. Check the relevant hostname, chain and redirects; a browser icon alone is not sufficient evidence. For certificate basics, see the guide to SSL certificates and website connections.
HSTS: A Time-Limited HTTPS Policy
What HSTS does
HTTP Strict Transport Security uses the Strict-Transport-Security response header. After receiving it over a valid HTTPS connection, a supporting browser upgrades requests for the covered hostname to HTTPS for the specified lifetime. Certificate errors then stop the connection. HSTS does not repair certificates, rewrite every external resource URL or encrypt a CDN-to-origin connection.
A short initial example is Strict-Transport-Security: max-age=300. The max-age is measured in seconds; later valid headers refresh the stored policy. includeSubDomains extends it to all subdomains, including internal or staging names. Preload is a separate opt-in and submission process; adding the token alone does not enroll a domain in a browser list.
How to Roll It Out Safely
After HTTPS and renewal work, begin with a short lifetime and expand only after testing. Audit subdomains before adding coverage. Removing the header does not immediately remove a browser’s stored policy, and preload removal has its own timeline. The MDN HSTS reference explains the behavior. Evaluate preload requirements separately from the starter configuration.
Server Config Examples
Nginx: place this directive in the HTTPS server block. It starts a five-minute policy for the current hostname, without subdomain coverage or preload. The always parameter also covers error responses.
add_header Strict-Transport-Security "max-age=300" always;Apache 2.4 with mod_headers: this condition applies the header only when TLS terminates directly at Apache. Use the virtual-host configuration or an .htaccess context permitted by the server. If TLS ends at a trusted reverse proxy, manage HSTS at the HTTPS-facing layer instead of blindly trusting a client-supplied scheme header.
Header always set Strict-Transport-Security "max-age=300" "expr=%{HTTPS} == 'on'"Express: with an existing app instance, register this middleware before routes. The example checks req.secure and assumes direct TLS or an explicitly configured, trusted proxy path. Do not enable blanket trust of forwarded headers to make this check pass.
app.use((req, res, next) => {
if (req.secure) {
res.set('Strict-Transport-Security', 'max-age=300');
}
next();
});Subdomains and proxy connections
A staging hostname under a domain with includeSubDomains inherits that policy even if staging does not send HSTS itself; shared cookies are not the reason. Also distinguish browser-to-CDN HTTPS from CDN-to-origin HTTPS. HSTS governs the browser, while origin encryption and certificate validation need separate proxy settings. For the architecture, see the CDN guide.
CSP: Resource and Document Rules
What a content security policy does
CSP defines policies for scripts, styles, images, fonts, connections and other document behavior. Different directives control different operations: frame-ancestors restricts who can embed this document, while frame-src restricts frames loaded inside it. A policy can block a required feature if its origins, inline code or resource types have not been accounted for.
Inventory the resources and important flows, then start with Report-Only. This monitors a candidate policy without enforcing it. Receiving server-side reports requires a working collector and reporting configuration; normal access logs do not automatically contain CSP violations. Keep testing login, checkout, administration and third-party integrations as the policy evolves.
A Complete Report-Only Example
The following are two HTTP response headers, each sent on one line. They are not Nginx or Apache directives. The example monitors same-origin resources and does not authorize inline scripts or styles. Replace example.com and the reporting path with a configured collector. The report-to name must match Reporting-Endpoints; report-uri provides compatibility for older reporting implementations.
Reporting-Endpoints: csp-endpoint="https://example.com/csp-report"
Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-reportThe collector must accept the relevant Reporting API and legacy CSP report formats. Do not add every reported origin automatically: confirm which resources the application needs. If inline scripts require nonces, generate an unpredictable value for each HTML response and insert it only into trusted script elements, for example through a template’s <script nonce="..."> attribute. The static policy above does not generate or include a nonce; the Express example below shows a separate dynamic approach.
Server Config Examples
Nginx, Report-Only: these directives publish the same candidate policy. Keep the reporting endpoint configured separately. Under default Nginx inheritance, a location with its own add_header directives does not inherit the parent set; inspect those responses and any explicit inheritance settings in your installed version.
add_header Reporting-Endpoints 'csp-endpoint="https://example.com/csp-report"' always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report" always;Apache with mod_headers, Report-Only: use an allowed configuration context and adapt the collector address. Set each header in one responsible layer and check for duplicates from the application, Apache or CDN.
Header always set Reporting-Endpoints "csp-endpoint=\"https://example.com/csp-report\""
Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report"Express with a response-specific nonce: this alternative uses a nonce-only script policy. Register the middleware on an existing app before HTML routes, and use the EJS template shown later. Every script you intend to authorize needs the matching nonce, including external scripts. The no-store header avoids reusing this dynamic HTML through ordinary caches; configure any CDN override consistently. This example still monitors rather than enforces CSP.
import crypto from 'node:crypto';
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.set('Cache-Control', 'no-store');
res.set('Reporting-Endpoints', 'csp-endpoint="https://example.com/csp-report"');
res.set('Content-Security-Policy-Report-Only',
`default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'nonce-${nonce}'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report`);
next();
});Handling Third-Party Scripts Without Losing Control
Allow only the external origins required by the application when using a hostname policy. A broad https: source permits any HTTPS host and is not a narrow allowlist. With a nonce-only script policy, a matching nonce authorizes a script regardless of a separate hostname list; adding an allowed host would create another route to permission, not an extra check. Replace inline event handlers with supported script-based listeners rather than adding unsafe-inline by default.
For directive behavior and policy design, use MDN’s CSP documentation and the OWASP CSP Cheat Sheet. Match their examples to the resources and framework you actually operate.
Framing Controls and Clickjacking
X-Frame-Options and CSP frame-ancestors
X-Frame-Options controls whether a document can be rendered inside a frame. DENY prohibits framing, and SAMEORIGIN requires matching origins throughout the ancestor chain. Origin includes scheme, hostname and port. ALLOW-FROM is obsolete. CSP frame-ancestors supports explicit external origins and checks every ancestor; it has no default-src fallback and must be delivered as a response header.
Choose the intended embedding scope
Use DENY with frame-ancestors none when the page must never be framed. The examples here choose SAMEORIGIN with frame-ancestors self to allow same-origin embedding. If a particular page must be embedded by an external application, define its allowed parent origins explicitly and test the response on that page. Loading a payment provider inside your page is a different frame-src question.
Server Config Examples
Nginx:
add_header X-Frame-Options "SAMEORIGIN" always;Apache:
Header always set X-Frame-Options "SAMEORIGIN"Express:
app.use((req, res, next) => {
res.set('X-Frame-Options', 'SAMEORIGIN');
next();
});An enforced CSP such as frame-ancestors 'none' takes precedence over X-Frame-Options in supporting browsers. Report-Only does not override it. Align DENY with none or SAMEORIGIN with self where those are intended. For external-embedding exceptions, an older client may still block the page under X-Frame-Options; retaining both headers is not automatically harmless for every use case.
X-Content-Type-Options and MIME Types
What nosniff changes
X-Content-Type-Options: nosniff tells the browser to respect the supplied Content-Type. For script and style destinations, a response with an unsuitable MIME type is blocked. It also restricts MIME sniffing in other contexts. It does not correct a misconfigured type, validate uploaded file contents or provide a general malware filter.
Server examples
Nginx:
add_header X-Content-Type-Options "nosniff" always;Apache:
Header always set X-Content-Type-Options "nosniff"Express:
app.use((req, res, next) => {
res.set('X-Content-Type-Options', 'nosniff');
next();
});Diagnose the actual response type
Check that CSS arrives as text/css and JavaScript as a recognized JavaScript MIME type. A script request that receives text/plain or an HTML error page may be blocked. A missing font or icon also warrants checks for the actual response, CORS and resource URL; do not attribute every font problem directly to nosniff.
A Staged Implementation Plan
Step 1: Map What You Actually Serve
List script, style, image, font and connection origins, inline code and required embedding relationships. Include authenticated pages and failure flows, not just the home page. Record which layer currently sets each header and how HTML responses are cached.
Step 2: Start with HTTPS and HSTS
Verify the hostname’s HTTPS endpoint, redirect path and certificate-renewal deployment. Start HSTS with a short max-age and audit subdomains before expanding the scope. Preloading is a separate commitment. See the MDN HSTS reference for the policy lifetime and removal behavior.
Step 3: Deploy CSP in Report-Only, Then Enforce
Deploy Report-Only with a working collector and review reports alongside browser tests. Choose hashes, response-specific nonces or a deliberately scoped hostname policy to match the application. When critical flows pass, change the candidate’s header name to Content-Security-Policy. A separate Report-Only policy can continue monitoring a future revision. Existing enforced policies remain active; a second CSP cannot loosen their restrictions.
Step 4: Add X-Frame-Options and X-Content-Type-Options
Apply the intended framing policy and correct Content-Type values with nosniff. Match SAMEORIGIN to frame-ancestors self, or DENY to none. Treat externally embeddable pages as explicit exceptions and account for the browsers that will use them.
Step 5: Inspect Responses and Browser Behavior
Inspect a real GET response rather than relying only on HEAD requests. Replace the example URL and check relevant success, redirect and error paths:
# Inspect headers for an actual GET response
curl -sS -D - -o /dev/null https://example.com/
# Select the security-header lines
curl -sS -D - -o /dev/null https://example.com/ | grep -Ei "^(strict-transport-security|content-security-policy(-report-only)?|reporting-endpoints|x-frame-options|x-content-type-options):"Inspect public response headers in the browser Network panel, count duplicate fields, and watch the console while exercising key flows. With a CDN, compare the public response to the origin as appropriate. curl can show headers but does not enforce CSP, perform clickjacking tests or prove that a report collector receives browser reports.
Step 6: Don’t Forget Your Infrastructure Basics
Headers complement access controls, patching and network protection. For broader maintenance, consult VPS server hardening and DDoS protection. Security headers do not limit incoming traffic volume or repair a compromised application.
Step 7: Consider the Edge: CDNs, Caches, and Multi-Environment Teams
Choose which layer owns each header. A CDN set operation can replace a value, while add can create another CSP that is enforced alongside the original. A rule that changes a nonce only in the header will not update the corresponding HTML. For dynamic nonce pages, keep the generated body and header together and configure caching accordingly. Subdomain HSTS inheritance also applies independently of cookies or environment labels.
Record the policy, collector ownership and validation steps in the deployment checklist. Retest after template, plugin, CDN or cache changes. The CDN guide explains the additional delivery layer these checks must cover.
Complete Starter Examples to Adapt
Nginx: Short HSTS and Monitored CSP
Place this set in the HTTPS server block after HTTPS is working. It permits same-origin framing and monitors a static same-origin CSP; it does not generate a nonce or permit inline code. Adapt the reporting address and configure its collector before expecting reports. Test child locations because default add_header inheritance can replace the parent set.
add_header Strict-Transport-Security "max-age=300" 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 Reporting-Endpoints 'csp-endpoint="https://example.com/csp-report"' always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report" always;Referrer-Policy: strict-origin-when-cross-origin sends the full referrer for same-origin requests, only the origin for secure cross-origin requests, and no referrer on an HTTPS-to-HTTP downgrade. It is an additional policy choice, separate from the four main headers.
Apache 2.4 with mod_headers
Header onsuccess unset Strict-Transport-Security
Header always set Strict-Transport-Security "max-age=300" "expr=%{HTTPS} == 'on'"
Header onsuccess unset X-Frame-Options
Header always set X-Frame-Options "SAMEORIGIN"
Header onsuccess unset X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"
Header onsuccess unset Referrer-Policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header onsuccess unset Reporting-Endpoints
Header onsuccess unset Content-Security-Policy-Report-Only
Header always set Reporting-Endpoints "csp-endpoint=\"https://example.com/csp-report\""
Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report"Express: Dynamic HTML and a Nonce
import crypto from 'node:crypto';
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.set('Cache-Control', 'no-store');
if (req.secure) {
res.set('Strict-Transport-Security', 'max-age=300');
}
res.set('X-Frame-Options', 'SAMEORIGIN');
res.set('X-Content-Type-Options', 'nosniff');
res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
res.set('Reporting-Endpoints', 'csp-endpoint="https://example.com/csp-report"');
res.set('Content-Security-Policy-Report-Only',
`default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'nonce-${nonce}'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; report-to csp-endpoint; report-uri /csp-report`);
next();
});EJS Template for the Express Examples
<script nonce="<%= cspNonce %>">
// Application-owned inline initialization
</script>
<script src="/app.js" nonce="<%= cspNonce %>"></script>The Express middleware must run before a route renders this EJS template so cspNonce is available from res.locals. Both script tags use the same nonce as that response’s policy; the next response needs a new value. Add nonces only to application-approved scripts, never to every tag in untrusted HTML. A valid nonce authorizes the script under this policy; it does not verify the script’s file integrity.
Debugging and Monitoring
DevTools First, Logs Second
Start with the public response and browser console. Then inspect the configured report collector, accounting for browser support and the report-to or legacy report-uri format. Group repeatable failures by directive, affected page and resource, and compare them with recent changes. Missing reports do not establish that no violations occurred.
Edge Cases with Third-Party Widgets
Payment and identity integrations may load nested frames and resources from several origins. Identify which operation each directive governs and permit only the required behavior. A dedicated route or hostname can simplify a complex integration’s policy. For unrelated WordPress runtime concerns, see server-side WordPress tuning; performance tuning does not validate CSP rules.
Don’t Forget the Big Picture
Use the specific failure as the starting point. For mixed content, correct stored resource URLs and verify HTTPS destinations. The upgrade-insecure-requests directive upgrades applicable requests when enforced; failed HTTPS does not fall back to HTTP, and a Report-Only policy does not perform that upgrade. The older block-all-mixed-content directive is obsolete and is omitted from these starters. Keep DNS, certificates and application maintenance in separate checks.
Deployment and Maintenance Checklist
Verify HTTPS, stage the HSTS lifetime, choose the required framing scope and serve correct content types. Monitor the candidate CSP with a configured collector, test the application, and enable enforcement only when those checks support it. Record the origin/CDN ownership and cache strategy for each response.
The Apache example uses an HTTPS expression for HSTS and clears the onsuccess header table before setting managed values in the always table. This addresses duplicate fields across Apache’s two header lists; it does not prevent another proxy from adding a header later. The Express example uses req.secure, so configure proxy trust only for the actual trusted path and ensure the proxy overwrites forwarded scheme headers.
Retest after framework, template or infrastructure changes, and include the public error paths in those checks. The guide to SSL certificates provides background for the HTTPS layer. Keep rollback expectations realistic, particularly for HSTS policies already stored by browsers.





