How to harden an Elasticsearch container (and is elasticsearch:8.16.1 safe for untrusted workloads?)¶
Short answer: a stock docker run elasticsearch:8.16.1 starts ahead of most images (it runs
non-root out of the box) but is still not a boundary to trust around an untrusted network.
Graded on IronClaw's seven-dimension containment scale, the default configuration scores
63 of 100, grade C (partial). Higher is safer. Three runtime flags take the same image to
100 of 100, grade A. This guide shows the exact gaps and the exact fixes, straight from the
scan data.
Every number here comes from a read-only
docker inspectofelasticsearch:8.16.1, the same data behind its isolation scorecard. No workload is executed. How scoring works →
Where the default configuration leaks¶
ironctl scan grades seven independent containment boundaries. On a default
docker run elasticsearch:8.16.1, three fail or warn:
| Dimension | Verdict | Score | What the scan found |
|---|---|---|---|
| Non-root user (uid != 0) | ✅ PASS | 15/15 | runs as 1000:0 (uid != 0) |
| Dropped capabilities | ❌ FAIL | 4/20 | default capability set retained (CAP_NET_RAW, CAP_MKNOD, and more) |
| Seccomp profile | ✅ PASS | 15/15 | seccomp profile active |
| Network isolation / egress | ⚠️ WARN | 4/15 | network=bridge: outbound egress is possible |
| Read-only root filesystem | ❌ FAIL | 0/10 | root filesystem is writable |
| No docker.sock exposure | ✅ PASS | 15/15 | no control socket mounted |
| No shared host namespaces | ✅ PASS | 10/10 | no host PID/IPC/network sharing |
Elasticsearch already runs as a non-root uid, which is why it clears the default 48/100 that
databases like MySQL and Postgres sit at. The remaining leaks are the retained capabilities
and open egress. An Elasticsearch that can reach the network is one that can exfiltrate every
document it indexes the moment a query DSL or plugin CVE lands code execution, and the full
capability set gives that code CAP_NET_RAW and friends to work with.
Harden it: the exact --fix remediation¶
ironctl scan my-elasticsearch --fix prints one remediation per failed dimension, then one
hardened run. For elasticsearch:8.16.1:
--cap-drop=ALL(Dropped capabilities, +16): drop every Linux capability; add back only what the workload provably needs. Elasticsearch needs none of the defaults.--network=none(Network isolation, +11): for a single-node index reached only by co-located services on a private user-defined network, cut host egress entirely; otherwise attach a single internal network with no default route, notbridge. (A multi-node cluster needs inter-node transport on a scoped private network, which is a WARN, not a clean pass, see the ceiling note below.)--user 65532:65532(Non-root user, already passing at +0): non-root already passes at 1000:0. Pinning an explicit fixed uid is the auditable choice and keeps the volume ownership unambiguous; it does not change the score.--read-only --tmpfs /tmp(Read-only rootfs, +10): make the root filesystem read-only and mount/usr/share/elasticsearch/dataas an explicit writable volume. Removes the persistence surface.
Before and after¶
# Before: 63/100, grade C
docker run -d --name elasticsearch \
-e discovery.type=single-node \
elasticsearch:8.16.1
# After: 100/100, grade A (single-node)
docker run -d --name elasticsearch-hardened \
-e discovery.type=single-node \
--user 65532:65532 \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp \
-v elasticsearch-data:/usr/share/elasticsearch/data \
--network=none \
elasticsearch:8.16.1
Rescan and the same seven dimensions all pass: ironctl scan elasticsearch-hardened reports
100/100 grade A. That is a 37-point swing from three one-line flags, no image rebuild.
The honest ceiling for a multi-node cluster¶
The 100/100 above is a single-node index with network=none. A multi-node Elasticsearch
cluster cannot use network=none: nodes must reach each other over the transport port. Attach a
private user-defined network scoped to just the cluster with no default route out, and the
network dimension scores 4 of 15 (a WARN, not a fail) instead of the full 15. That puts a
hardened multi-node cluster at 89 of 100, grade B, one point off an A. The other six
dimensions still max out. That is the honest ceiling when nodes must talk, and it is a long way
from the default C.
Verify it on your own cluster¶
# install (Homebrew)
brew install ironsecco/ironclaw/ironclaw
# grade your running container, then print the fixes
ironctl scan my-elasticsearch
ironctl scan my-elasticsearch --fix
ironctl scan also reads a docker-compose.yml service or a Kubernetes manifest, so you can
grade the Elasticsearch in your stack, not just a bare docker run.
Keep going¶
- elasticsearch:8.16.1 isolation scorecard →: the full dimension breakdown.
- Search engines, ranked by isolation →: how Elasticsearch compares to OpenSearch, Solr, Meilisearch, and the rest.
- How to harden an nginx container →: another service with an honest egress ceiling, explained the same way.
- Scan any container in 10 seconds →: the full
ironctl scanreference. - Run untrusted code in a real sandbox →: IronClaw wraps every AI-agent session in a gVisor/Kata boundary with
network=noneby default.