LaunchDarkly is one of those tools that looks harmless from a CSP perspective until it quietly breaks in production. The SDK initializes, flags never arrive, and the only clue is a blocked request buried in DevTools.
I’ve seen this a lot with frontend teams that already have a decent CSP and assume feature flags are “just another script.” They usually aren’t. LaunchDarkly needs network access for streaming, polling, events, and sometimes bootstrapping behavior that doesn’t fit neatly into a locked-down policy.
If you’re running a developer-facing site and want LaunchDarkly without punching a giant hole in your policy, these are the common mistakes I’d fix first.
Mistake #1: Only allowing the SDK script, not the network endpoints
This is the most common breakage.
Teams add the LaunchDarkly SDK source to script-src and stop there. The script loads fine, but the browser SDK still needs to talk to LaunchDarkly APIs. If connect-src doesn’t allow those endpoints, flags won’t update and events won’t flush.
A real-world header often looks like this:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NjljNmVhMGEtODVmNy00MTBiLTk1NWYtMmM3MTdiOWVjNTVh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://u.headertest.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That policy already shows the pattern: third-party integrations usually need connect-src, not just script-src.
For LaunchDarkly, the browser SDK usually needs network access for:
- flag evaluation / SDK config
- analytics and diagnostic events
- streaming updates
A typical fix looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
connect-src 'self'
https://app.launchdarkly.com
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
img-src 'self' data:;
style-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Exact LaunchDarkly endpoints depend on which SDK and service mode you use, so check the official docs for your SDK before hardcoding anything. But the rule is simple: if the SDK fetches, streams, or posts events, connect-src is where the real work happens.
Mistake #2: Forgetting Server-Sent Events or WebSocket requirements
A lot of teams lock down connect-src to HTTPS endpoints and then wonder why live flag updates don’t work.
LaunchDarkly’s browser-side streaming commonly relies on EventSource. Some deployments may also involve WebSocket-like patterns in other tooling. If your app depends on real-time updates, you need to verify the actual transport your SDK uses and allow it.
For example, people often do this:
connect-src 'self' https://events.launchdarkly.com https://clientstream.launchdarkly.com;
Looks reasonable. But if your browser session is opening a different streaming origin or transport, CSP blocks it.
If the SDK requires secure websocket connections, you’d need something like:
connect-src 'self'
https://events.launchdarkly.com
https://clientstream.launchdarkly.com
wss://clientstream.launchdarkly.com;
If it uses EventSource over HTTPS, https://... may be enough. Don’t guess. Open DevTools, filter by Fetch/XHR/EventStream/WS, and watch what actually gets blocked.
This is one of those places where CSP debugging is less about theory and more about looking at the browser and believing what it tells you.
Mistake #3: Falling back to default-src and assuming it covers everything
It technically might, but it usually creates a mess.
I still see policies that rely heavily on default-src:
Content-Security-Policy:
default-src 'self' https://*.launchdarkly.com;
That’s lazy and brittle. default-src acts as a fallback for some directives, but once you care about security, you want explicit rules. Feature flag SDKs need network access, not broad permissions across scripts, images, frames, and whatever else happens to inherit from default-src.
A tighter version is better:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
connect-src 'self'
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
img-src 'self' data:;
style-src 'self';
object-src 'none';
base-uri 'self';
If you want a good refresher on directive boundaries, https://csp-guide.com is useful.
Mistake #4: Whitelisting broad wildcards like https://*.launchdarkly.com
I get why people do it. You’re trying to get staging unstuck, the SDK is failing, and a wildcard feels like the fastest path.
It’s also usually wider than you need.
This:
connect-src 'self' https://*.launchdarkly.com;
is much better than:
connect-src *;
but it still gives away more trust than necessary. If your app only needs a couple of LaunchDarkly origins, list those exact origins.
Prefer this:
connect-src 'self'
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
Same story for script-src. Don’t add LaunchDarkly there unless you are actually loading a remote script from LaunchDarkly. If you bundle the SDK with your app, script-src doesn’t need any LaunchDarkly host at all.
That distinction matters. I’ve seen teams cargo-cult third-party CSP entries for months because nobody stopped to ask whether the browser ever loaded a script from that host.
Mistake #5: Breaking your nonce model with inline LaunchDarkly bootstrap code
A lot of frontend apps bootstrap LaunchDarkly state server-side to avoid startup flicker. That usually means an inline script block that injects user context or preloaded flags into the page.
Then CSP blocks it.
Example of the pattern:
<script>
window.__LD_BOOTSTRAP__ = {
flags: { newNavbar: true },
context: { kind: "user", key: "123" }
};
</script>
If your policy uses nonces, that script needs the matching nonce:
<script nonce="{{ .CSPNonce }}">
window.__LD_BOOTSTRAP__ = {
flags: { newNavbar: true },
context: { kind: "user", key: "123" }
};
</script>
And the header:
Content-Security-Policy:
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
If you’re already running a strict nonce-based policy like the headertest.com example, stay consistent. Don’t “fix” one blocked inline block by adding 'unsafe-inline'. That’s how good CSPs slowly rot.
Also, be careful with how you serialize bootstrap data. If user-controlled values get embedded into that inline block without proper escaping, you’ve just turned your LaunchDarkly bootstrap into an XSS sink.
Mistake #6: Using the client-side SDK key like it changes CSP needs
I’ve heard this argument more than once: “It’s a client-side ID, so CSP isn’t really relevant.”
Wrong mental model.
The client-side ID being public doesn’t change the browser behavior. CSP doesn’t care whether a key is secret. It cares where scripts execute and where the page can connect.
This code still needs CSP allowances:
import * as LDClient from "launchdarkly-js-client-sdk";
const client = LDClient.initialize("client-side-id", {
kind: "user",
key: "user-123"
});
client.on("ready", () => {
const enabled = client.variation("new-navbar", false);
console.log("new-navbar:", enabled);
});
The SDK initialization triggers outbound connections. If connect-src is wrong, your public key won’t save you.
Mistake #7: Forgetting local development origins
Production CSP gets most of the attention, but local dev is where people start adding terrible exceptions.
Typical dev setup:
- app on
http://localhost:3000 - Vite or webpack dev server
- LaunchDarkly browser SDK making HTTPS requests
- maybe hot reload over WebSocket
Then someone jams this into the dev header:
connect-src *;
script-src * 'unsafe-inline' 'unsafe-eval';
That “temporary” change tends to survive way longer than anyone wants to admit.
A cleaner dev policy is explicit about what changed:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self'
http://localhost:3000
ws://localhost:3000
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
img-src 'self' data:;
object-src 'none';
I don’t love 'unsafe-eval', but some dev toolchains still need it. Keep it out of production.
Mistake #8: Debugging from the app instead of CSP reports and the browser console
When LaunchDarkly fails under CSP, app logs are usually vague. The SDK may just look “not ready” or return fallback values.
The browser console tells the real story:
- which directive was violated
- which origin was blocked
- whether it was
script-src,connect-src, or something else
If you’re using Content-Security-Policy-Report-Only, even better. Roll out LaunchDarkly changes in report-only mode first, gather violations, then tighten the enforced policy.
A practical rollout looks like this:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
connect-src 'self'
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
report-to default-endpoint;
Then verify:
- SDK initializes
- flag values arrive
- streaming updates work
- event posting works
- no unexpected extra origins appear
That last one matters. If adding LaunchDarkly suddenly requires broad wildcard exceptions, stop and re-check your integration.
A sane starting policy
If you bundle the LaunchDarkly browser SDK yourself and only need its network endpoints, this is a good baseline:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
connect-src 'self'
https://clientstream.launchdarkly.com
https://events.launchdarkly.com;
img-src 'self' data:;
style-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
Then adjust based on what your browser actually shows for your LaunchDarkly SDK version and configuration.
That’s really the whole game with CSP and feature flags: be precise, trust the network panel more than assumptions, and don’t weaken script-src just because connect-src was the real problem.