docker.io/grafana/grafana
validated pin current tags: 13.0.2
grafana — capabilities AND read-only-filesystem, derived by drop-test. capabilities: cap_drop:[ALL] + cap_add:[] — ZERO-cap. grafana is a non-root service (uid 472) on the unprivileged :3000 writing only to its /var/lib/grafana data store, so none of the Docker default 14 caps is load-bearing. Published as an explicit dimension because a user running the STOCK image has the default 14 caps and has not dropped anything: "you can cap_drop:ALL, cap_add:[]" is a real 14 -> 0 reduction, even though the hardened reference compose already drops them. filesystem: read_only:true, tmpfs:[]. grafana runs correctly under read_only:true; its data store /var/lib/grafana (sqlite DB + any installed plugins) is a PERSISTENT VOLUME (must survive restarts, NOT tmpfs). The derived tmpfs minimum is EMPTY. filesystem: read_only:true, tmpfs:[]. Under a read-only rootfs with only the /var/lib/grafana data volume writable, grafana serves the UI, reads/writes its sqlite DB, and runs backend datasource plugins (prometheus/Loki) — /tmp was drop-tested and is NOT required for that. A writable /tmp IS used by features NOT exercised here — grafana's runtime plugin *installation*, the elasticsearch datasource plugin, and image rendering / data export — so a deployment using those should add tmpfs:[/tmp] (the reference compose keeps it conservatively). Hence confidence is moderate: grafana's feature surface is large and this workload cannot bound all of it. See criteria/docker.io/grafana/grafana.md.
Use it
services:
grafana:
image: docker.io/grafana/grafana:13.0.2
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
read_only: true
This profile has multiple dimensions and is applied as a unit. Dimensions can interact — a capability can be required only because of a sibling read-only/tmpfs recommendation — so where they do, the minimum was derived under the sibling dimension's context. See the criteria doc and each dimension's recorded invocation below before applying them separately.
Dimension: capabilities
cap_drop: ALL (no cap_add)
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/grafana/grafana@sha256:5dad0df181cb… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/grafana.sh |
| workload sha256 | d41070b8ff46… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
CHOWN | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
DAC_OVERRIDE | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
FSETID | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
FOWNER | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
MKNOD | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
NET_RAW | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
SETGID | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
SETUID | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
SETFCAP | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
SETPCAP | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
NET_BIND_SERVICE | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
SYS_CHROOT | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
KILL | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
AUDIT_WRITE | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
Recorded invocation (run_config)
The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.
| security_opt | no-new-privileges:true |
|---|---|
| mounts | grafana-data:/var/lib/grafana |
| env | GF_SECURITY_ADMIN_PASSWORDGF_ANALYTICS_REPORTING_ENABLED |
Workload script (profiles/workloads/grafana.sh)
#!/usr/bin/env bash
# Workload exerciser for the grafana reference image.
#
# Drives grafana's real function under a read-only rootfs (with /var/lib/grafana
# writable — the persistent data volume): the database is reachable, a dashboard
# can be written and read back (a real sqlite write on the data volume), and a
# backend datasource plugin runs. Returns 0 if every step succeeds.
#
# SCOPE: this covers grafana core + datasource querying + DB. A writable /tmp is
# used only by grafana's runtime plugin *installation*, the elasticsearch
# datasource plugin, and image rendering / data export — not exercised here (see
# the criteria doc).
#
# Required env: GRAFANACONTAINER (target container name or id). The container is
# expected to have been started with GF_SECURITY_ADMIN_PASSWORD set.
set -euo pipefail
: "${GRAFANACONTAINER:?GRAFANACONTAINER must be set}"
C="${GRAFANACONTAINER}"
B='http://admin:$GF_SECURITY_ADMIN_PASSWORD@localhost:3000'
deadline=$((SECONDS + 45))
until docker exec "$C" sh -c "curl -sf \"$B/api/health\" 2>/dev/null | grep -q '\"database\": \"ok\"'"; do
if (( SECONDS >= deadline )); then
echo "grafana /api/health never reported database ok" >&2
exit 1
fi
sleep 1
done
uid="$(docker exec "$C" sh -c "curl -s -X POST -H 'Content-Type: application/json' -d '{\"dashboard\":{\"title\":\"csd-probe\",\"panels\":[]},\"overwrite\":true}' \"$B/api/dashboards/db\"" 2>/dev/null | sed -n 's/.*\"uid\":\"\([^\"]*\)\".*/\1/p' | head -1)"
if [ -z "$uid" ]; then
echo "dashboard create failed (DB write path broken)" >&2
exit 1
fi
docker exec "$C" sh -c "curl -sf -o /dev/null \"$B/api/dashboards/uid/$uid\"" 2>/dev/null || { echo "dashboard read-back failed" >&2; exit 1; }
duid="$(docker exec "$C" sh -c "curl -s -X POST -H 'Content-Type: application/json' -d '{\"name\":\"csd-loki\",\"type\":\"loki\",\"access\":\"proxy\",\"url\":\"http://127.0.0.1:9\"}' \"$B/api/datasources\"" 2>/dev/null | sed -n 's/.*\"uid\":\"\([^\"]*\)\".*/\1/p' | head -1)"
body="$(docker exec "$C" sh -c "curl -s \"$B/api/datasources/uid/$duid/health\"" 2>/dev/null)"
grep -q '"status"' <<<"$body" || { echo "Loki datasource backend plugin not running: ${body:0:120}" >&2; exit 1; }
Dimension: filesystem
read_only: true, no tmpfs
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/grafana/grafana@sha256:5dad0df181cb… |
| validated date | 2026-07-04 |
| confidence | moderate |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/grafana.sh |
| workload sha256 | d41070b8ff46… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
/tmp | removable | correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running |
Recorded invocation (run_config)
The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.
| security_opt | no-new-privileges:true |
|---|---|
| mounts | grafana-data:/var/lib/grafana |
| env | GF_SECURITY_ADMIN_PASSWORDGF_ANALYTICS_REPORTING_ENABLED |
Workload script (profiles/workloads/grafana.sh)
#!/usr/bin/env bash
# Workload exerciser for the grafana reference image.
#
# Drives grafana's real function under a read-only rootfs (with /var/lib/grafana
# writable — the persistent data volume): the database is reachable, a dashboard
# can be written and read back (a real sqlite write on the data volume), and a
# backend datasource plugin runs. Returns 0 if every step succeeds.
#
# SCOPE: this covers grafana core + datasource querying + DB. A writable /tmp is
# used only by grafana's runtime plugin *installation*, the elasticsearch
# datasource plugin, and image rendering / data export — not exercised here (see
# the criteria doc).
#
# Required env: GRAFANACONTAINER (target container name or id). The container is
# expected to have been started with GF_SECURITY_ADMIN_PASSWORD set.
set -euo pipefail
: "${GRAFANACONTAINER:?GRAFANACONTAINER must be set}"
C="${GRAFANACONTAINER}"
B='http://admin:$GF_SECURITY_ADMIN_PASSWORD@localhost:3000'
deadline=$((SECONDS + 45))
until docker exec "$C" sh -c "curl -sf \"$B/api/health\" 2>/dev/null | grep -q '\"database\": \"ok\"'"; do
if (( SECONDS >= deadline )); then
echo "grafana /api/health never reported database ok" >&2
exit 1
fi
sleep 1
done
uid="$(docker exec "$C" sh -c "curl -s -X POST -H 'Content-Type: application/json' -d '{\"dashboard\":{\"title\":\"csd-probe\",\"panels\":[]},\"overwrite\":true}' \"$B/api/dashboards/db\"" 2>/dev/null | sed -n 's/.*\"uid\":\"\([^\"]*\)\".*/\1/p' | head -1)"
if [ -z "$uid" ]; then
echo "dashboard create failed (DB write path broken)" >&2
exit 1
fi
docker exec "$C" sh -c "curl -sf -o /dev/null \"$B/api/dashboards/uid/$uid\"" 2>/dev/null || { echo "dashboard read-back failed" >&2; exit 1; }
duid="$(docker exec "$C" sh -c "curl -s -X POST -H 'Content-Type: application/json' -d '{\"name\":\"csd-loki\",\"type\":\"loki\",\"access\":\"proxy\",\"url\":\"http://127.0.0.1:9\"}' \"$B/api/datasources\"" 2>/dev/null | sed -n 's/.*\"uid\":\"\([^\"]*\)\".*/\1/p' | head -1)"
body="$(docker exec "$C" sh -c "curl -s \"$B/api/datasources/uid/$duid/health\"" 2>/dev/null)"
grep -q '"status"' <<<"$body" || { echo "Loki datasource backend plugin not running: ${body:0:120}" >&2; exit 1; }
Evidence & provenance
- Validation criteria — per-image scenarios and pass criteria
- Profile source in the repository
- Version history — every previously published pin, with the full profile as validated against it
Raw profile YAML
# grafana — capabilities AND read-only-filesystem, derived by drop-test.
#
# capabilities: cap_drop:[ALL] + cap_add:[] — ZERO-cap. grafana is a non-root
# service (uid 472) on the unprivileged :3000 writing only to its
# /var/lib/grafana data store, so none of the Docker default 14 caps is
# load-bearing. Published as an explicit dimension because a user running the
# STOCK image has the default 14 caps and has not dropped anything: "you can
# cap_drop:ALL, cap_add:[]" is a real 14 -> 0 reduction, even though the
# hardened reference compose already drops them.
#
# filesystem: read_only:true, tmpfs:[]. grafana runs correctly under
# read_only:true; its data store /var/lib/grafana (sqlite DB + any installed
# plugins) is a PERSISTENT VOLUME (must survive restarts, NOT tmpfs). The
# derived tmpfs minimum is EMPTY.
#
# filesystem: read_only:true, tmpfs:[]. Under a read-only rootfs with only the
# /var/lib/grafana data volume writable, grafana serves the UI, reads/writes its
# sqlite DB, and runs backend datasource plugins (prometheus/Loki) — /tmp was
# drop-tested and is NOT required for that. A writable /tmp IS used by features NOT
# exercised here — grafana's runtime plugin *installation*, the elasticsearch
# datasource plugin, and image rendering / data export — so a deployment using
# those should add tmpfs:[/tmp] (the reference compose keeps it conservatively).
# Hence confidence is moderate: grafana's feature surface is large and this workload
# cannot bound all of it. See criteria/docker.io/grafana/grafana.md.
schema_version: "1.5"
image: docker.io/grafana/grafana
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/grafana/grafana.html
applies_to:
tags: ["13.0.2"]
status: validated
dimensions:
capabilities:
cap_drop: [ALL]
cap_add: []
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/grafana/grafana@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
validated_date: "2026-07-16"
duration_seconds: 30
confidence: high
workload: profiles/workloads/grafana.sh
workload_sha256: "d41070b8ff46d1fa4153e005712752546f7b411caf7cb70dc9f239116e840135"
validated_via: [drop-test, ci-smoke]
observation_backend:
ig_version: v0.51.0
gadgets: []
run_config:
user: ""
command: []
entrypoint: ""
network: ""
pid: ""
devices: []
security_opt: ["no-new-privileges:true"]
mounts: ["grafana-data:/var/lib/grafana"]
env: [GF_SECURITY_ADMIN_PASSWORD, GF_ANALYTICS_REPORTING_ENABLED]
drop_test:
checks:
- {removed: CHOWN, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: DAC_OVERRIDE, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: FSETID, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: FOWNER, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: MKNOD, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: NET_RAW, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: SETGID, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: SETUID, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: SETFCAP, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: SETPCAP, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: NET_BIND_SERVICE, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: SYS_CHROOT, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: KILL, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
- {removed: AUDIT_WRITE, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}
filesystem:
read_only: true
tmpfs: []
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/grafana/grafana@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
validated_date: "2026-07-04"
duration_seconds: 4
confidence: moderate
workload: profiles/workloads/grafana.sh
workload_sha256: "d41070b8ff46d1fa4153e005712752546f7b411caf7cb70dc9f239116e840135"
validated_via: [drop-test, ci-smoke]
observation_backend:
ig_version: v0.51.0
gadgets: []
run_config:
user: ""
command: []
entrypoint: ""
network: ""
pid: ""
devices: []
security_opt: ["no-new-privileges:true"]
mounts: ["grafana-data:/var/lib/grafana"]
env: [GF_SECURITY_ADMIN_PASSWORD, GF_ANALYTICS_REPORTING_ENABLED]
drop_test:
checks:
- {removed: /tmp, required: false, observed: "correct: /api/health db ok, dashboard write+read-back, backend datasource plugin running"}