CSP for Elasticsearch Frontends: Options, Tradeoffs, and Gotchas
Table of Contents
If you put Elasticsearch behind a browser-facing frontend, your Content Security Policy gets tricky fast.
The problem usually is not Elasticsearch itself. It’s the frontend around it: Kibana, a custom React app, embedded dashboards, analytics tags, cookie banners, WebSocket connections, and a build pipeline that quietly injects inline scripts when you are not looking.
I’ve seen teams lock down default-src 'self' and feel good for five minutes, then discover their search UI needs connect-src for the cluster API, wss: for live updates, and some awful third-party script exception because marketing won an argument.
This guide compares the main CSP approaches for Elasticsearch frontends, with pros, cons, and practical examples.
What makes Elasticsearch frontends special
A typical Elasticsearch frontend usually needs some combination of:
- XHR or
fetch()to Elasticsearch or a backend proxy - WebSockets for live updates
- inline bootstrapping scripts from modern frontend frameworks
- Kibana assets and embedded frames
- third-party analytics or consent tooling
- strict restrictions against framing, plugins, and form posting
That means your CSP often lives or dies on these directives:
connect-srcscript-srcframe-srcframe-ancestorsstyle-srcbase-uriobject-srcform-action
A real-world policy from headertest.com shows what “production reality” looks like:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MzAzMDZmYjItYWM1Mi00NmM0LWE4MGYtYTEyNzJiYjFkNTNl' '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://tallycdn.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 header is a good reminder: clean CSPs get messy when business requirements show up.
Option 1: Direct browser-to-Elasticsearch with a narrow connect-src
This is the simplest architecture. Your frontend talks directly to Elasticsearch APIs.
Example
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://es.example.com;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none'
Pros
- Simple to reason about
- Easy to map CSP to actual network flows
- Fewer moving parts than adding a proxy layer
- Good fit for internal tools
Cons
- Exposes Elasticsearch endpoint details to the browser
- Harder to control auth and request shaping
connect-srchas to allow the cluster endpoint directly- CORS becomes part of your security story whether you like it or not
My take
I don’t love this for internet-facing apps. It’s workable for internal dashboards, especially on trusted networks, but it pushes too much responsibility to the browser. If your frontend only needs search results, a backend proxy is usually the cleaner move.
Option 2: Frontend to backend proxy, backend to Elasticsearch
This is what I recommend most of the time. The browser talks only to your app backend, and the backend talks to Elasticsearch.
Example
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none'
If your app uses live updates over WebSockets:
connect-src 'self' wss://app.example.com;
Pros
- Best separation between browser and Elasticsearch
- Easier auth, rate limiting, logging, and query validation
- Much tighter
connect-src - No need to expose Elasticsearch hostnames to the client
Cons
- More backend code
- One more hop in the request path
- Can become a bottleneck if implemented badly
My take
This is the sweet spot. Your CSP stays smaller, your app architecture is safer, and you can prevent users from sending arbitrary Elasticsearch queries from the browser. That matters more than people think.
Option 3: Kibana frontend with CSP exceptions
If you are serving Kibana or embedding Kibana content, your CSP decisions are often constrained by Kibana’s asset loading and plugin model.
Example for self-hosted Kibana UI
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self' https://kibana.example.com https://es-proxy.example.com wss://kibana.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
Pros
- Practical when Kibana is already the frontend
- Less custom code
- Good for operational dashboards and internal observability
Cons
- You may need
unsafe-inlineinstyle-src - Plugins can expand your policy surface
- Embeds and cross-origin setups get messy
- Harder to keep the policy minimal over time
My take
Kibana is operationally convenient, but it is not where I’d expect a beautifully tiny CSP. Be realistic. Start strict, test every feature, and document every exception. Otherwise the policy turns into cargo cult paste.
Option 4: Embedded Elasticsearch dashboards in another app
A lot of teams want to embed dashboards in a portal or admin app. This introduces frame-src and frame-ancestors decisions.
Example: your app embeds Kibana
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-src https://kibana.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
On the Kibana side, you may need to allow your portal as a framing ancestor.
Pros
- Clean separation between app and dashboards
- Easier to roll out without rebuilding analytics UI
- Good for internal reporting portals
Cons
- Framing rules are easy to get wrong
- Auth across iframe boundaries can be painful
- CSP debugging becomes split across two apps
- Clickjacking protections need coordination
My take
Embeds are fine, but they’re where teams accidentally weaken frame-ancestors or over-broaden frame-src. If you only need charts, rendering them natively in your app is often cleaner than iframing an entire dashboard product.
Script policy choices: nonce vs hash vs unsafe-inline
This is where most Elasticsearch frontends win or lose.
Best option: nonces
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
Pros
- Strong protection against injected inline scripts
- Works well with server-rendered bootstrapping code
strict-dynamicreduces hostname allowlist sprawl
Cons
- Requires server-side nonce generation and template integration
- Can be awkward with static hosting
Hashes
script-src 'self' 'sha256-abc123...';
Pros
- Great for truly static inline snippets
- No per-request nonce generation
Cons
- Painful if scripts change often
- Build pipelines need discipline
unsafe-inline
script-src 'self' 'unsafe-inline';
Pros
- Easy
- Makes broken apps work immediately
Cons
- Weakens CSP badly
- Usually a sign you gave up
My opinion: if you are building a modern search UI, use nonces. Hashes are okay for static fragments. unsafe-inline in script-src is usually a bad trade.
Third-party tooling: the policy bloat factory
The headertest.com policy is a classic example. Once analytics and consent tooling arrive, your CSP gets wider:
www.googletagmanager.com*.google-analytics.com*.cookiebot.comconsent.cookiebot.comconsentcdn.cookiebot.com
That can be legitimate, but every extra origin is another trust decision. For Elasticsearch frontends, I try to keep the search experience isolated from marketing dependencies when possible.
A trimmed version of a “realistic but controlled” policy might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com wss://app.example.com https://*.google-analytics.com;
frame-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That is still broader than I’d like, but at least the risky parts are explicit.
Recommended baseline for most Elasticsearch frontends
If you have a custom frontend and a backend proxy, start here:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' wss://app.example.com;
frame-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
Then add only what the app proves it needs.
If you need ready-made patterns, the examples at https://csp-examples.com can save time. For directive behavior and browser semantics, the official reference is the MDN and CSP spec documentation, and Elasticsearch/Kibana deployment details should come from official Elastic docs.
Practical decision guide
Use direct browser-to-Elasticsearch when:
- the app is internal
- you control the network
- the query surface is limited
- you can keep
connect-srctight
Use a backend proxy when:
- the app is public
- you care about query control
- you want the cleanest CSP
- you need better auth and auditability
Use Kibana with exceptions when:
- speed matters more than perfect CSP minimalism
- the operational UI already exists
- your users are mostly internal
Use embeds when:
- dashboards are separate from the core app
- iframe auth and framing rules are understood
- you are willing to debug two CSPs, not one
If I had to pick one default architecture for a developer audience building an Elasticsearch frontend today, I’d choose: custom frontend, backend proxy, nonce-based script-src, narrow connect-src, and no third-party scripts unless someone can defend them in a security review. That setup gives you the best balance of usability and control without turning CSP into decorative header theater.