xola/elasticsearch-proxy-bundle
A Symfony bundle that adds a proxy layer in front of Elasticsearch, simplifying index and query handling for PHP applications. It wraps client access behind bundle configuration so services can search and index without direct Elasticsearch wiring.
The first thing to internalize: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/elasticsearch-proxy.php, and composer require xola/elasticsearch-proxy-bundle in a Laravel app will pull in symfony/framework-bundle, symfony/http-kernel, and friends without doing anything useful. Treat it as a reference architecture + deployable sidecar, not a dependency.
Two viable paths, in order of effort:
Run it as a sidecar (recommended). Clone the repo, containerize the Symfony app, point it at your Elasticsearch cluster, and have Laravel talk to it instead of ES. Your config/elasticsearch.php becomes:
'hosts' => [env('ELASTICSEARCH_PROXY_URL')],
and ES itself is never reachable from your app network. This is how you get the authorization guarantees without porting anything.
Mine it for the authorization logic. Read the request listener / controller that intercepts _search calls and the voter (or equivalent authorization service) that decides what a principal may see. Re-implement the query-rewriting in a Laravel middleware or a decorator around Elasticsearch\Client. Expect to rewrite, not port — the DI wiring won't transfer.
Where to look first: the bundle's Resources/config/services.* for the wiring graph, then the proxy controller — that's the whole product in one class. Then the security/voter layer for the rules you'll want to mirror.
First use case: multi-tenant index access. A SaaS app where every document carries a company_id / tenant_id, and you cannot trust the client to filter by it. The proxy stamps the filter on the way through, so a malicious or buggy query still can't cross tenants.
Sidecar + signed context header. Laravel authenticates the user, mints a short-lived HMAC or JWT containing tenant_id and allowed index patterns, and attaches it as a header. The proxy validates the signature, discards any client-supplied tenant filter, and injects its own. Never let the proxy trust a plain X-Tenant-Id header — it must be signed or the whole exercise is theater.
A thin client wrapper in Laravel. Rather than sprinkling the official client around, bind a single SearchGateway:
$this->app->bind(SearchGateway::class, fn () => new SearchGateway(
ClientBuilder::create()->setHosts([config('elasticsearch.proxy')])->build()
));
Give it search(), index(), bulk() methods with typed params. When you later swap the proxy for ES's native security features, you change one class.
Query-time rewriting over per-tenant indices. Two schools: (a) one index with a mandatory term filter per request, or (b) index-per-tenant with the proxy rewriting the _index path. (b) is cleaner for deletes and mappings but multiplies shard count; (a) is what most teams land on. The proxy approach supports either — pick before you write the Laravel side.
Filtered aliases as a cheap alternative. If your needs are coarse (index-level, not document-level), filtered aliases plus one alias per tenant gives you most of the isolation with zero proxy code. Reach for the proxy only when you need per-document or per-field rules.
Middleware for context propagation. Stamp the tenant/auth header in a global middleware so queued jobs and console commands use the same path — those are the calls that silently bypass authorization in most implementations, because they build queries by hand.
Health checks and circuit breaking. Laravel's HTTP layer should treat the proxy as a dependency: a Health check hitting the proxy's ping endpoint, plus a timeout + retry policy. A dead proxy should degrade search, not take down checkout.
Zero dependents, last release November 2020. No one is maintaining this. Pin the commit, fork it, and own the patches. Budget for ES 8 incompatibilities — the 2020-era code targets 6.x/7.x, and mapping types, _doc, and several APIs changed.
Rewriting _search is the easy 20%. The authorization surface also includes _msearch (multiple query bodies in one payload), _bulk, _search/template, q= query-string params, scroll and PIT contexts, and painless scripts. A proxy that filters only _search is bypassable in about four ways. Verify each endpoint is either rewritten or rejected — an allowlist of permitted APIs is safer than a denylist.
Reject, don't sanitize, unknown query shapes. If a request body contains a construct your rewriter doesn't understand, return 400. Silent pass-through is how tenant leaks ship.
Debugging the rewrite. Log the inbound and outbound bodies at the proxy (redact credentials), and diff them. When results look wrong, the question is almost always "did the filter get applied twice" — duplicate term filters are harmless but duplicate must_not clauses are not.
Network isolation is the actual security boundary. The proxy only means something if ES is unreachable from anywhere except the proxy's network segment — security groups, no public endpoint, no shared VPC peering to app subnets. Otherwise you've added latency and called it authorization.
Latency budget. You've added a hop and a Symfony bootstrap per request. Keep the proxy warm (opcache, php-fpm workers), enable persistent HTTP connections to ES, and measure p99 before and after. If search is on the critical path of page load, consider whether per-tenant filtered aliases get you there cheaper.
Extension points worth knowing. The bundle's authorization decision is the seam you'll customize — that's where your rules go. If you port to Laravel instead, the analogue is a Gate/Policy plus a query-builder decorator; keep the decision logic out of the controller.
Testing. Stand up an ES test container with two tenants' worth of fixtures, then assert that every endpoint returns zero cross-tenant hits — including _msearch and _bulk. Write these as the first tests; they're the spec for whatever you build on top.
Sanity check before committing. If your rule is genuinely "tenant A can only see tenant A's documents," Elasticsearch's built-in document-level security and API keys (7.x+) may cover it with far less code. This bundle was written when those weren't an option. Confirm the gap before you inherit an unmaintained proxy.
How can I help you explore Laravel packages today?