catalog / docker.io/grafana/grafana

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)

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/grafana/grafana@sha256:5dad0df181cb…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/grafana.sh
workload sha256d41070b8ff46…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
CHOWNremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
DAC_OVERRIDEremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
FSETIDremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
FOWNERremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
MKNODremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
NET_RAWremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
SETGIDremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
SETUIDremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
SETFCAPremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
SETPCAPremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
NET_BIND_SERVICEremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
SYS_CHROOTremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
KILLremovablecorrect: /api/health db ok, dashboard write+read-back, backend datasource plugin running
AUDIT_WRITEremovablecorrect: /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_optno-new-privileges:true
mountsgrafana-data:/var/lib/grafana
envGF_SECURITY_ADMIN_PASSWORD
GF_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

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/grafana/grafana@sha256:5dad0df181cb…
validated date2026-07-04
confidencemoderate
validated viadrop-test, ci-smoke
workloadprofiles/workloads/grafana.sh
workload sha256d41070b8ff46…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
/tmpremovablecorrect: /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_optno-new-privileges:true
mountsgrafana-data:/var/lib/grafana
envGF_SECURITY_ADMIN_PASSWORD
GF_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

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"}