View Categories

The Content Security Policy

A Content Security Policy (CSP) is a response header that tells the browser which origins a page may load scripts, styles, images, frames and network connections from. RackHelm builds its policy in PHP, per request, so pages that legitimately need to reach a host nobody could know in advance can widen it for that one response. This article is for whoever administers the server.

Where the policy comes from #

The base policy lives in config/security.php, under the csp key. On every web response, RackHelm’s security-headers middleware builds the header from that config and sets it unconditionally.

Directive Allowed sources out of the box
default-src 'self'
script-src 'self', 'unsafe-inline', 'unsafe-eval', Stripe.js, Google Tag Manager, Cloudflare Turnstile
style-src 'self', 'unsafe-inline'
img-src 'self', data:, https:
frame-src 'self', Stripe.js, Stripe hooks, Cloudflare Turnstile
connect-src 'self', the Stripe API, Google Tag Manager and the Google Analytics endpoints

Note: script-src still permits 'unsafe-inline' and 'unsafe-eval'. RackHelm’s admin views carry a great deal of inline script; tightening that is a separate piece of work and not something you can do from a setting.

Third-party front-end libraries are self-hosted rather than pulled from a CDN precisely so the policy can stay this narrow. Stripe is the deliberate exception.

Why it is built per request #

Some pages talk to hosts that cannot be listed in a config file. The clearest example is the game-panel console: the browser opens a websocket straight to the daemon on whichever node the customer’s server happens to live on, at an address like wss://node3.example.com:8080. No static allowlist can know those hosts, and they change as you add nodes.

Those pages therefore widen connect-src while they render, and the middleware emits the widened policy for that response only. Every other response keeps the base policy. That mechanism is the reason the next section matters so much.

Never add a second CSP header #

Warning: Do not add a Content-Security-Policy header in public/.htaccess, in your nginx server block, or anywhere else in front of the application. When a browser receives two CSP headers it applies the intersection of them — a source must be permitted by both policies to be allowed. A static copy in the web server would therefore silently strip every runtime addition, and the only symptom is a feature that quietly stops working: the console never connects, a payment frame never appears, and the browser console shows a CSP violation for a host you can see in the header RackHelm sent.

RackHelm’s shipped public/.htaccess declares the other security headers but deliberately declares no CSP. If you inherit a modified .htaccess, or a server block written for a different application, check it before debugging anything else:

grep -ri "content-security-policy" /etc/nginx/ /var/www/rackhelm/public/.htaccess

The expected result is a comment only, and nothing that sets the header.

The other security headers behave differently #

The middleware sets Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy only if they are absent. That makes it a no-op on Apache, where .htaccess already sets them, and a safety net everywhere else — nginx with PHP-FPM, for instance, never executes .htaccess at all, so without the middleware the site would ship with no security headers.

Widening the policy for your own install #

If your install fronts a host RackHelm cannot discover on its own — a self-hosted analytics endpoint, a chat widget, a panel node you want allowlisted permanently — use the environment escape hatch rather than editing the config file. Each directive has a matching variable that is merged into the base policy:

Variable Widens
CSP_DEFAULT_SRC default-src
CSP_SCRIPT_SRC script-src
CSP_STYLE_SRC style-src
CSP_IMG_SRC img-src
CSP_FRAME_SRC frame-src
CSP_CONNECT_SRC connect-src

Values are space- or comma-separated. To allowlist a panel node’s websocket permanently, add both schemes:

CSP_CONNECT_SRC="wss://node1.example.com:8080 https://node1.example.com:8080"

Sources containing whitespace, a semicolon or a comma are discarded when the policy is assembled, so a malformed entry cannot append directives of its own.

Note: RackHelm’s installer deliberately does not cache configuration, so an edit to .env takes effect on the next request. If you have run php artisan config:cache yourself, re-run it after editing.

Checking what is actually being sent #

  1. Open the affected page in your browser and press F12 to open developer tools.
  2. On the Network tab, click the document request and read the response headers. There must be exactly one Content-Security-Policy entry.
  3. On the Console tab, read the violation message. It names the directive that blocked the request and the URL that was blocked — which tells you which variable above to widen.

From the command line:

curl -sI https://portal.example.com/ | grep -i content-security-policy
Browser developer tools showing a single Content-Security-Policy response header
One header, not two. Two means something in front of the application is adding its own.

Troubleshooting #

Symptom Cause and fix
The game-panel console never connects; the console logs a connect-src violation for a wss:// URL Almost always a second CSP header from the web server intersecting away the runtime addition. Remove it. Failing that, add the node explicitly with CSP_CONNECT_SRC.
Two Content-Security-Policy headers in the response Something in front of the application is setting one. Check .htaccess, your nginx or Apache configuration, and any CDN or proxy in front of the portal.
A third-party widget’s script is blocked Add its origin to CSP_SCRIPT_SRC, and its API host to CSP_CONNECT_SRC if it also makes requests.
An entry you added to .env is missing from the header Either it contains a space, comma or semicolon inside the source and was discarded, or your configuration is cached. Quote the whole value, separate sources with spaces, and re-run config:cache if you use it.
Images from an external host are blocked Unlikely — img-src already allows all of https:. Check the URL is not plain http:.
Card fields fail to load at checkout Stripe’s script and frame origins must remain in the policy. If you have overridden config/security.php, restore the Stripe entries.