How to Implement Content Security Policy (CSP) Headers in Nginx, Apache, and Node.js

Content Security Policy often enters an enterprise backlog as a security-header task. It rarely stays that simple.

A modern enterprise application may load JavaScript from multiple internal domains, analytics platforms, identity providers, CDNs, customer-support tools, payment systems, experimentation platforms, and increasingly AI-enabled services. Add legacy inline scripts and multiple deployment layers, and a CSP that looks correct in a security review can break production within minutes.

That makes CSP implementation less about copying a header and more about deciding which code the browser should trust, who controls that trust, and how the organization changes the policy without disrupting customer journeys.

The timing matters. Verizon’s 2026 Data Breach Investigations Report found that exploitation of vulnerabilities became the leading breach entry point at 31%, while third-party involvement reached 48% of breaches. CSP does not solve those categories by itself, but the trend reinforces why browser-side execution controls and third-party dependency governance deserve more attention.

What Should an Enterprise Content Security Policy Actually Protect?

CSP tells browsers where applications may load or execute resources such as JavaScript, stylesheets, images, fonts, frames, and network connections.

The simplest policy might begin with:

Content-Security-Policy: default-src 'self';

That is useful as a conceptual starting point, but enterprise applications normally need more granular controls.

A production policy may look closer to:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';

OWASP recommends strict CSP approaches based on nonces or hashes rather than relying entirely on source allowlists. strict-dynamic can extend trust from a nonce-authorized script to scripts that it subsequently loads.

This distinction matters for large applications. Allowing an entire script host because one dependency needs it can create considerably more trust than the architecture intended.

MDN also notes that when strict-dynamic is active with an appropriate nonce or hash, host sources and values such as 'self' are ignored for the affected script trust chain in supporting browsers.

The practical objective therefore is not creating the longest CSP. It is creating the smallest maintainable trust boundary that still supports the application.

How Should CSP Headers Be Configured in Nginx?

For applications where Nginx controls the final HTTP response, CSP can be added directly in the server configuration.

server {
    listen 443 ssl;
    server_name app.example.com;

    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self';
        style-src 'self';
        img-src 'self' data:;
        object-src 'none';
        base-uri 'none';
        frame-ancestors 'none';
    " always;

    location / {
        proxy_pass http://application_backend;
    }
}

The always parameter deserves attention. Nginx documentation states that without it, add_header applies only to specified response codes. With always, the header is added regardless of the response status.

Header inheritance is another common enterprise failure point. Nginx normally inherits add_header directives from the previous configuration level only when the current level defines no add_header directives of its own. A team can therefore add an unrelated header inside a nested location block and unintentionally lose the CSP inherited from the server block.

That behavior should be covered by automated response-header tests, particularly when hundreds of routes or multiple platform teams share the same ingress configuration.

Dynamic nonces introduce another architectural question. Nginx can distribute a static policy easily, but applications that generate a fresh nonce per HTML response usually need coordination between the rendering layer and the response header. In those environments, CSP ownership may belong closer to Node.js or another application framework rather than the reverse proxy.

How Should CSP Headers Be Added in Apache?

Apache deployments can implement CSP through mod_headers:

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "
        default-src 'self';
        script-src 'self';
        style-src 'self';
        img-src 'self' data:;
        object-src 'none';
        base-uri 'none';
        frame-ancestors 'none';
    "
</IfModule>

Apache distinguishes between the default onsuccess response-header table and always. Its documentation recommends understanding this distinction carefully because headers can otherwise behave differently across normal responses, redirects, errors, and internally generated responses.

For enterprise platforms, CSP testing therefore cannot stop at the homepage returning HTTP 200.

Engineering teams should verify at least three conditions:

  • Successful application responses need the intended CSP rather than merely any CSP. Tests should verify exact high-risk directives such as script-src, object-src, base-uri, frame-ancestors, and external connection destinations. This prevents configuration drift from quietly broadening browser permissions.
  • Redirects, authentication failures, error pages, and edge-generated responses should receive appropriate protection. Otherwise, different parts of the customer journey may operate under different security assumptions even though they share the same domain.
  • Upstream services, CDN rules, Apache configuration, and application middleware should not independently emit conflicting policies. Multiple policies are enforced together by browsers rather than treated as simple replacements, which can cause difficult production failures when infrastructure ownership is fragmented.

That last problem frequently appears during platform modernization because security headers accumulate at several layers over time.

How Should Node.js Generate a Nonce-Based CSP?

Node.js gives application teams finer control because the application can generate a unique nonce for each response and place the same value in both the CSP header and trusted script elements.

An Express implementation can use Node’s cryptographic random generator:

const express = require("express");
const crypto = require("node:crypto");

const app = express();

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString("base64");
  res.locals.cspNonce = nonce;

  res.set(
    "Content-Security-Policy",
    `default-src 'self';
     script-src 'nonce-${nonce}' 'strict-dynamic';
     style-src 'self';
     img-src 'self' data:;
     object-src 'none';
     base-uri 'none';
     frame-ancestors 'none';`
  );

  next();
});

A server-rendered template can then use:

<script nonce="{{cspNonce}}" src="/assets/application.js"></script>

Node documents crypto.randomBytes() as generating cryptographically strong pseudorandom data, while Express provides res.set() for setting response headers.

The nonce must be generated for each response. It should not become an application constant, environment variable, or reusable deployment secret.

OWASP also warns against middleware that blindly inserts the nonce into every script tag. If attacker-controlled markup reaches that transformation step, the injected script could receive the trusted nonce too.

This is why CSP design must remain connected to the rendering architecture instead of becoming an infrastructure-only configuration task.

How Can CSP Be Rolled Out Without Breaking Production?

A strict policy should rarely move directly from configuration review into enforcement.

Content-Security-Policy-Report-Only lets browsers evaluate a candidate policy and report violations without blocking the resources. MDN documents this specifically as a testing mechanism, while current CSP implementation guidance recommends collecting real traffic before enforcement.

A rollout might therefore begin with:

Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'nonce-...' 'strict-dynamic';
object-src 'none';
base-uri 'none';
report-to csp-endpoint;

Teams can then investigate violations from production traffic, regional services, experimentation tools, marketing tags, customer-support integrations, and less frequently visited application routes.

This phase should not become an exercise in allowing every reported hostname. Reports should trigger a decision: Is the dependency legitimate? Can it be self-hosted? Does an entire origin need trust? Can a nonce or hash replace that trust?

Once violations stabilize, the policy can move into enforcement while a tighter candidate continues in Report-Only mode. That creates a continuous hardening path instead of a one-time security project.

Who Should Own CSP Across a Large Enterprise Platform?

This is where many technically correct CSP implementations deteriorate.

Platform engineering may own Nginx. Infrastructure teams may control CDN headers. Application teams may own nonces. Marketing controls tag managers. Security sets policy requirements. Product teams continuously introduce new third-party dependencies.

Without explicit ownership, CSP eventually becomes either too restrictive to change safely or so permissive that it offers limited protection.

The operating model matters as much as the directive syntax.

Organizations that lack the internal capacity to map these dependencies sometimes involve external engineering and security consulting partners. Firms such as GeekyAnts, Thoughtworks, and Accenture operate in broader digital engineering and technology consulting environments where browser security controls can be addressed alongside application architecture, modernization, DevSecOps, and platform engineering.

The important selection criterion is not whether a partner can write a CSP header. Most engineering teams can do that.

The harder question is whether the architecture can maintain a strict policy across application releases, multiple delivery layers, legacy code, third-party dependencies, and several engineering teams without turning every frontend change into a security exception.

For a VP of Engineering or Head of Platform Engineering, that is the useful point for an architecture review. A focused consultation can examine the current script trust model, delivery layers, nonce feasibility, third-party dependencies, reporting strategy, and ownership boundaries before enforcement reaches production.

A CSP header may occupy one line in an HTTP response. The engineering system required to keep that line accurate is considerably larger.

Leave a Reply

Your email address will not be published. Required fields are marked *