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-src
  • script-src
  • frame-src
  • frame-ancestors
  • style-src
  • base-uri
  • object-src
  • form-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-src has 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-inline in style-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-dynamic reduces 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.com
  • consent.cookiebot.com
  • consentcdn.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.

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-src tight

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.