How to harden a Grafana container (and is grafana/grafana:11.2.0 safe next to your metrics?)¶
Grafana is the window into every other system you run: it holds datasource credentials, query
access to your metrics and logs, and often SSO tokens for the humans who log in. A stock
docker run grafana/grafana:11.2.0 gets one thing right and three things wrong. Graded on
IronClaw's seven-dimension containment scale, the default configuration scores 63 of 100, grade C
(weak). Higher is safer. The image already runs as a non-root uid, which is more than most images
on this directory manage, but it keeps the full capability set and a writable root filesystem. A few
runtime flags take the same image to 89 of 100, grade B, one point off an A. The one dimension it
cannot reach is the one a dashboard server needs by definition: browsers have to connect to it. Here
are the exact gaps and fixes from the scan data.
Graded from a read-only inspect of a running container started from
grafana/grafana:11.2.0with plaindocker rundefaults, its entrypoint overridden withsleeppurely to keep it alive. The scan itself executes nothing inside the container. It is the same data behind its isolation scorecard. How scoring works →
Where the default configuration leaks¶
ironctl scan grades seven independent containment boundaries. On a default docker run
grafana/grafana:11.2.0, two fail and one warns:
| Dimension | Verdict | Score | What the scan found |
|---|---|---|---|
| Non-root user (uid != 0) | ✅ PASS | 15/15 | runs as 472 (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 |
The two that should worry you most are capabilities and egress. Grafana renders untrusted
input: dashboard JSON, plugin code, and datasource responses all flow through it, and its plugin
system runs backend binaries. A rendering or plugin CVE that lands code execution inherits
CAP_NET_RAW and the rest of the default set, and from a container that can reach arbitrary
destinations it can quietly ship your datasource credentials and query results out. The writable
rootfs is the persistence surface that makes such a foothold durable.
Harden it: the exact --fix remediation¶
ironctl scan my-grafana --fix prints one remediation per failed dimension, then one hardened run.
For grafana/grafana:11.2.0:
--cap-drop=ALL(Dropped capabilities, +16): drop every Linux capability. Grafana needs none of the default set to serve dashboards; it binds to a high port by default.--read-only --tmpfs /tmp(Read-only rootfs, +10): make the root filesystem read-only and mount the data path (/var/lib/grafana) as an explicit writable volume this uid owns. Removes the tamper and persistence surface.- Scoped network (Network isolation):
--network=nonescores the full 15 but is wrong for a dashboard server, browsers and its datasources must reach it. Any named or bridge network scores 4 of 15 (a WARN, not a fail): a connection path exists. Contain it anyway: put Grafana on a user-defined network scoped to just the reverse proxy in front of it and the datasources behind it, with no default route out, so a compromised Grafana cannot call arbitrary internet addresses.
The uid is already non-root, so the non-root dimension is a PASS out of the box. Keep it that way:
point the data volume at the 472 uid rather than reverting to root to dodge a permissions error.
Before and after¶
# Before: 63/100, grade C
docker run -d --name grafana grafana/grafana:11.2.0
# After: 89/100, grade B (scoped private network for browsers and datasources)
docker run -d --name grafana-hardened \
--user 472:472 \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--read-only --tmpfs /tmp \
-v grafana-data:/var/lib/grafana \
--network=observability-internal \
grafana/grafana:11.2.0
Rescan: ironctl scan grafana-hardened reports 89/100 grade B. A 26-point swing with no custom
image build, just the right flags. The only dimension still short of full marks is the network (4 of
15), because a dashboard server exists to be connected to; network=none would score the last points
but leave no browser able to load a panel. That is the honest ceiling for a UI service, and it is a
clear step up from the default C.
Verify it on your own Grafana¶
# install (Homebrew)
brew install ironsecco/ironclaw/ironclaw
# grade your running container, then print the fixes
ironctl scan my-grafana
ironctl scan my-grafana --fix
ironctl scan also reads a docker-compose.yml service or a Kubernetes manifest, so you can grade
the Grafana in your stack, not just a bare docker run.
Keep going¶
- All hardening guides →: every harden-a-container walkthrough, with grade deltas.
- grafana:11.2.0 isolation scorecard →: the full dimension breakdown.
- How to harden a Prometheus container →: the metrics store Grafana usually queries, with the same honest ceiling.
- 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.