The first time I deployed Elasticsearch for a client project, I made the mistake of exposing port 9200 directly to the internet during testing and forgot to lock it down before launch. Within a day, I found unfamiliar indices in the cluster that I hadn’t created. That experience taught me, the hard way, why you should never expose Elasticsearch directly and why putting Nginx in front of it as a reverse proxy is close to mandatory for any production deployment.
Elasticsearch doesn’t ship with built-in authentication in many self-managed setups (unless you’re using the security features in newer versions or a paid tier), and even when it does, teams often want an additional layer of control — IP restriction, rate limiting, TLS termination, and basic auth — sitting in front of it. Nginx handles all of that well. In this guide, I’ll walk through exactly how to set this up properly, securely, and in a way that won’t leave you finding surprise indices in your cluster.
Why Put Nginx in Front of Elasticsearch?
Elasticsearch’s REST API listens on port 9200 by default, and it’s a powerful, largely unauthenticated interface out of the box in many open-source deployments. That means anyone who can reach that port can create indices, delete data, run arbitrary queries, and potentially exfiltrate your entire dataset. Reverse proxying through Nginx gives you:
- TLS termination — Elasticsearch’s basic setup doesn’t always have TLS configured, and doing it at the Nginx layer is simpler to manage alongside your other services.
- Authentication — HTTP basic auth or client certificate authentication can be bolted on without touching Elasticsearch’s own configuration.
- IP allowlisting — restrict access to known internal networks or specific external IPs (like your office or CI/CD pipeline).
- Rate limiting — protect against abusive query patterns or accidental request storms from misbehaving clients.
- URL-level access control — restrict which API endpoints are reachable. For instance, you might expose search endpoints but block index deletion entirely for certain user groups.
- Centralized logging — Nginx’s access logs give you a single place to review who’s querying your cluster and how often.
Requirements
Before starting, you’ll need:
- A running Elasticsearch cluster (I’ll assume single-node for simplicity, but this applies to clustered setups too — you’d typically point Nginx at a coordinating node or load balance across multiple nodes)
- Nginx installed on the same host or a separate proxy host that can reach Elasticsearch’s port
- A domain name if you want proper TLS (or self-signed certs for internal-only use)
- Basic familiarity with your Elasticsearch cluster’s current network binding — check
elasticsearch.ymlfor thenetwork.hostsetting
Confirm Elasticsearch is running:
curl -X GET "http://localhost:9200/?pretty"
You should see cluster name, version, and tagline information returned as JSON.
Step 1: Lock Down Elasticsearch’s Own Network Exposure
Before touching Nginx, make sure Elasticsearch itself isn’t already listening on a public interface. Edit /etc/elasticsearch/elasticsearch.yml:
network.host: 127.0.0.1
http.port: 9200
Binding Elasticsearch to localhost ensures it’s only reachable from the same machine, which is exactly where your Nginx instance will be making requests from (assuming a single-host setup). Restart Elasticsearch after this change:
sudo systemctl restart elasticsearch
If Nginx and Elasticsearch live on different hosts, bind Elasticsearch to a private network interface instead, and make sure your firewall rules only allow the Nginx host to reach port 9200.
Step 2: Install Nginx and the Apache Utilities Package (for Basic Auth)
sudo apt update
sudo apt install nginx apache2-utils -y
On CentOS/RHEL:
sudo dnf install nginx httpd-tools -y
Step 3: Create a Basic Auth Credentials File
This is the simplest and most common way to add an authentication layer:
sudo htpasswd -c /etc/nginx/.es_htpasswd elastic_admin
You’ll be prompted to set a password. Drop the -c flag if you’re adding additional users to an existing file later.
Step 4: Write the Nginx Server Block
Here’s a complete configuration for a reverse proxy with TLS, basic auth, and reasonable timeouts:
server {
listen 443 ssl;
server_name es.example.com;
ssl_certificate /etc/letsencrypt/live/es.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/es.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
auth_basic "Elasticsearch Access";
auth_basic_user_file /etc/nginx/.es_htpasswd;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:9200;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
server {
listen 80;
server_name es.example.com;
return 301 https://$host$request_uri;
}
A few things worth calling out:
proxy_read_timeout 300s;— Elasticsearch queries, especially aggregations over large datasets, can take a while. The default 60-second timeout will cause legitimate slow queries to fail with a 504. Adjust this based on your actual query patterns.client_max_body_size 20m;— bulk indexing requests can be large. If you’re doing heavy bulk ingestion through this proxy, you may need to raise this further.proxy_http_version 1.1;withConnection "";— this enables connection reuse (keepalive) between Nginx and Elasticsearch, reducing overhead on high-throughput setups.
Step 5: Restrict Dangerous Endpoints (Optional but Recommended)
If you want an extra layer of protection beyond authentication — say, allowing search queries but blocking destructive operations like index deletion from most users — you can add specific location blocks:
location ~ ^/.*/_delete_by_query {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.es_admin_htpasswd;
proxy_pass http://127.0.0.1:9200;
}
location ~ ^/_cluster/(settings|reroute) {
deny all;
}
location / {
proxy_pass http://127.0.0.1:9200;
proxy_set_header Host $host;
}
This pattern lets you apply a stricter credential set (or an outright block) to sensitive administrative endpoints, while general search traffic uses lighter restrictions.
Step 6: Test and Reload
sudo nginx -t
sudo systemctl reload nginx
Test from an external machine:
curl -u elastic_admin -k https://es.example.com/_cluster/health?pretty
You should be prompted for the password you set earlier, and after entering it, you should see cluster health JSON returned.
Adding IP Allowlisting
If your Elasticsearch cluster only needs to be reachable from specific known networks (your office, a CI/CD runner, application servers), combine basic auth with IP restriction for defense in depth:
location / {
allow 203.0.113.0/24;
allow 198.51.100.10;
deny all;
auth_basic "Elasticsearch Access";
auth_basic_user_file /etc/nginx/.es_htpasswd;
proxy_pass http://127.0.0.1:9200;
proxy_set_header Host $host;
}
Rate Limiting to Protect Against Abuse
Elasticsearch queries can be expensive, and a runaway script or a misconfigured client hammering your cluster can cause real performance problems. Nginx’s rate limiting can act as a safety valve:
limit_req_zone $binary_remote_addr zone=es_limit:10m rate=20r/s;
server {
location / {
limit_req zone=es_limit burst=40 nodelay;
proxy_pass http://127.0.0.1:9200;
proxy_set_header Host $host;
}
}
This allows 20 requests per second per client IP, with a burst allowance of 40 before requests start getting rejected with a 503.
Testing the Setup Thoroughly
Beyond the basic curl test above, I recommend testing several scenarios:
- Unauthenticated request — should return 401:
curl -i https://es.example.com/_cluster/health - Authenticated request — should succeed:
curl -i -u elastic_admin https://es.example.com/_cluster/health - A slow aggregation query — verify it doesn’t time out prematurely given your
proxy_read_timeoutsetting. - A bulk indexing request larger than the default body size — verify your
client_max_body_sizeis sufficient, or you’ll see a 413 error. - Access from a disallowed IP (if you configured allowlisting) — should be rejected outright, ideally without even reaching the auth prompt.
Troubleshooting Common Issues
Problem: 502 Bad Gateway.
This almost always means Nginx can’t reach Elasticsearch on the backend. Verify Elasticsearch is running and listening where expected:
sudo ss -tlnp | grep 9200
Check that network.host in elasticsearch.yml matches what your proxy_pass directive expects.
Problem: 504 Gateway Timeout on legitimate queries.
Increase proxy_read_timeout and proxy_send_timeout. Complex aggregations or large scroll queries can legitimately take longer than default values.
Problem: 413 Request Entity Too Large.
Raise client_max_body_size in your server or location block. This is common with bulk indexing operations.
Problem: Basic auth prompt keeps reappearing even with correct credentials.
Double check the htpasswd file permissions (Nginx’s worker process needs read access) and confirm there’s no typo in the auth_basic_user_file path. Also verify you didn’t accidentally leave a stray auth_basic off; in a nested location block overriding your setting.
Problem: Kibana or another Elasticsearch client can’t connect through the proxy.
Some clients don’t handle basic auth prompts gracefully or expect specific headers. Check the client’s documentation for how it passes credentials, and consider using proxy_set_header Authorization pass-through carefully if the client is sending its own auth headers that need to reach Elasticsearch’s built-in security features (if enabled).
Security Considerations
- Never rely on obscurity. Just changing the port or hiding the URL isn’t a substitute for actual authentication.
- Use TLS everywhere, especially if credentials are being sent over the wire. Don’t run this over plain HTTP in production.
- Rotate credentials periodically and avoid sharing a single basic auth user across multiple applications or team members — create separate accounts where practical.
- Consider Elasticsearch’s native security features (available in many current versions, including the free tier of the Elastic Stack) as a complementary layer rather than a replacement for the proxy. Defense in depth matters here.
- Audit your Nginx access logs regularly. Unusual query patterns, repeated 401s from the same IP, or spikes in traffic to destructive endpoints are all worth investigating.
- Disable dangerous endpoints entirely if your use case doesn’t need them exposed externally at all — things like
_cluster/settings,_snapshot, or index deletion routes are common candidates for outright blocking at the Nginx layer for non-admin users.
Performance Tips
- Enable
keepaliveconnections between Nginx and Elasticsearch to cut down on connection setup overhead for high-throughput environments. - If you’re proxying to multiple Elasticsearch nodes for load balancing, use an
upstreamblock with a sensible load balancing method:upstream es_cluster { server 10.0.0.11:9200; server 10.0.0.12:9200; server 10.0.0.13:9200; keepalive 64;} - Monitor Nginx’s own resource usage under load — a busy Elasticsearch proxy can become a bottleneck itself if worker processes or connection limits aren’t tuned appropriately (
worker_connectionsin the events block). - Compress responses where appropriate with
gzip on;, especially for large JSON payloads returned from search queries, to reduce bandwidth usage for remote clients.
Real-World Use Cases
- Internal analytics dashboards querying Elasticsearch from a browser-based Kibana-like tool, where you don’t want to expose the raw Elasticsearch API to end users without an auth layer.
- Log aggregation pipelines (ELK/EFK stacks) where log shippers from various servers need to reach Elasticsearch securely over the internet, and TLS plus basic auth at the Nginx layer is simpler than managing certificates on the Elasticsearch side directly.
- Multi-tenant SaaS platforms where different customer-facing services need scoped access to different indices, enforced partly through URL-path restrictions at the proxy layer.
- CI/CD integration testing where automated test suites need to reach a shared Elasticsearch instance from outside the internal network, but only from known CI runner IP ranges.
Best Practices Summary
- Bind Elasticsearch to localhost or a private interface; never expose it directly.
- Terminate TLS at Nginx and enforce HTTPS redirects.
- Layer authentication (basic auth at minimum) on top of IP restrictions where feasible.
- Set generous but bounded timeouts to accommodate legitimate slow queries without leaving the door open indefinitely.
- Block or separately restrict destructive administrative endpoints.
- Log everything and review those logs periodically.
- Test both the happy path and the failure modes (wrong credentials, oversized payloads, disallowed IPs) before considering the setup production-ready.
Logging and Auditing in More Depth
I like to set up a dedicated log format for the Elasticsearch proxy so I can actually make sense of query patterns later, rather than sifting through Nginx’s default combined log format:
log_format es_proxy '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_user_agent" rt=$request_time';
server {
access_log /var/log/nginx/elasticsearch_access.log es_proxy;
error_log /var/log/nginx/elasticsearch_error.log warn;
# ... rest of config
}
The rt=$request_time field is genuinely useful here — it tells you how long each request took end to end, which helps you distinguish “this query is just slow because it’s a big aggregation” from “something’s wrong with the proxy or the cluster.” I’d recommend rotating these logs aggressively (logrotate with daily rotation and compression) since a busy Elasticsearch proxy can generate a lot of log volume, especially if you’re logging every search request from an application doing frequent lookups.
Frequently Asked Questions
Do I still need this proxy if I’m using Elasticsearch’s built-in security features?
Often, yes, at least partially. Elasticsearch’s native security (available in current versions at no extra cost for the basic tier) handles authentication and role-based access control well, but it doesn’t give you the same flexibility for things like IP allowlisting, rate limiting, or TLS certificate management integrated with the rest of your infrastructure. Many teams run both together: native security for fine-grained access control within the cluster, and Nginx for network-level protections and centralized TLS handling.
Can I proxy Kibana through the same Nginx instance?
Yes, and it’s a common pattern — just use a separate server_name or path prefix pointing at Kibana’s port (usually 5601) with its own auth and TLS configuration, distinct from the Elasticsearch API proxy itself. Keep them as separate location or server blocks since Kibana and the raw Elasticsearch API have different security postures and typical audiences (internal analysts versus application services, for instance).
Is basic auth secure enough for this?
Combined with TLS, HTTP basic auth is reasonably secure for internal or semi-trusted use cases, but it’s not a substitute for proper credential management at scale — no per-user audit trail beyond what you log yourself, no fine-grained permissions, and credentials that are annoying to rotate across many clients. For anything beyond a small team or a handful of service accounts, I’d lean toward Elasticsearch’s native role-based security or an API gateway with more robust auth tooling.
What if my Elasticsearch cluster has multiple nodes and I want the proxy to load balance across them?
Define an upstream block listing each node’s address and reference it in proxy_pass instead of a single backend. Just be aware that not every node in a cluster is necessarily a good target for client queries — some deployments designate specific “coordinating” nodes for handling client requests, so check your cluster’s topology before load balancing indiscriminately across every node.
Why do I sometimes get inconsistent results between two different requests to the same query right after indexing new data?
This is normal Elasticsearch behavior, not a proxy issue — Elasticsearch is near-real-time, not immediately consistent, and newly indexed documents typically become searchable within about a second, not instantly. It’s worth knowing this so you don’t waste time debugging your Nginx config for what’s actually expected Elasticsearch refresh behavior.
Wrapping Up
Elasticsearch is a powerful tool, but its default posture assumes a trusted network, which is rarely the reality once you’re running anything in production. Putting Nginx in front of it as a reverse proxy gives you a well-understood, battle-tested layer for authentication, TLS, and access control without having to reconfigure Elasticsearch’s own internals extensively. It took me finding those unexpected indices to really internalize this lesson, but hopefully this guide saves you from learning it the same way. Take the time to test your setup against both legitimate and malicious traffic patterns before you consider it done.