docker.io/grafana/loki
validated pin current tags: 3.7.2
loki — capabilities AND read-only-filesystem, derived by drop-test. capabilities: cap_drop:[ALL] + cap_add:[] — ZERO-cap. loki is a non-root distroless service (uid 10001) on the unprivileged :3100 writing only to its /loki data store, so no capability is load-bearing. Published explicitly so a user of the stock image (which has Docker's default 14 caps) sees the 14 -> 0 reduction, not just the filesystem dimension. filesystem: read_only:true, tmpfs:[] — /loki is a PERSISTENT VOLUME (NOT tmpfs); the derived tmpfs minimum is EMPTY. filesystem: read_only:true, tmpfs:[]. Under a read-only rootfs with only its /loki data dir writable, loki initialises all modules, becomes ready, ingests a pushed log, and serves it back on query — with a read-only /tmp (drop-tested NOT required). /loki holds chunks + WAL + index + rules and is a PERSISTENT VOLUME (must survive restarts, NOT tmpfs). loki is DISTROLESS (no shell/curl), so it is validated from a sidecar probe sharing its network namespace, not `docker exec` — see the criteria doc.
Use it
services:
loki:
image: docker.io/grafana/loki:3.7.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/loki@sha256:191d4fdfb726… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/loki.sh |
| workload sha256 | 1d77529ff984… |
| 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: loki ready, log push (204) + query round-trip returned the line |
DAC_OVERRIDE | removable | correct: loki ready, log push (204) + query round-trip returned the line |
FSETID | removable | correct: loki ready, log push (204) + query round-trip returned the line |
FOWNER | removable | correct: loki ready, log push (204) + query round-trip returned the line |
MKNOD | removable | correct: loki ready, log push (204) + query round-trip returned the line |
NET_RAW | removable | correct: loki ready, log push (204) + query round-trip returned the line |
SETGID | removable | correct: loki ready, log push (204) + query round-trip returned the line |
SETUID | removable | correct: loki ready, log push (204) + query round-trip returned the line |
SETFCAP | removable | correct: loki ready, log push (204) + query round-trip returned the line |
SETPCAP | removable | correct: loki ready, log push (204) + query round-trip returned the line |
NET_BIND_SERVICE | removable | correct: loki ready, log push (204) + query round-trip returned the line |
SYS_CHROOT | removable | correct: loki ready, log push (204) + query round-trip returned the line |
KILL | removable | correct: loki ready, log push (204) + query round-trip returned the line |
AUDIT_WRITE | removable | correct: loki ready, log push (204) + query round-trip returned the line |
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 | loki-data:/loki |
Workload script (profiles/workloads/loki.sh)
#!/usr/bin/env bash
# Workload exerciser for the loki reference image.
#
# loki is DISTROLESS (no shell/curl), so it is probed from a SIDECAR container that
# shares loki's network namespace (`docker run --network container:<target>`) and
# reaches it on localhost:3100 — rather than `docker exec`, which has nothing to run
# inside a distroless image. The probe image is a pinned, multi-arch curl.
#
# Drives loki's real read+write path under a read-only rootfs (with /loki writable —
# the persistent data volume): push a log line and query it back. Returns 0 on
# success.
#
# Required env: LOKICONTAINER (target container name or id).
set -euo pipefail
: "${LOKICONTAINER:?LOKICONTAINER must be set}"
C="${LOKICONTAINER}"
# curlimages/curl:8.11.1 — multi-arch, digest-pinned (test-only probe image).
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 60))
until [ "$(sc -s http://localhost:3100/ready 2>/dev/null)" = ready ]; do
if (( SECONDS >= deadline )); then
echo "loki /ready never reported ready in 60s" >&2
exit 1
fi
sleep 2
done
ts="$(date +%s%N)"
code="$(sc -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' \
--data "{\"streams\":[{\"stream\":{\"job\":\"csd\"},\"values\":[[\"$ts\",\"csd-probe-line\"]]}]}" \
http://localhost:3100/loki/api/v1/push 2>/dev/null)"
if [ "$code" != 204 ]; then
echo "loki push returned HTTP $code (expected 204)" >&2
exit 1
fi
start=$((ts - 300000000000)); end=$((ts + 300000000000))
for _ in 1 2 3 4 5; do
# capture-then-match via herestring — never `sc | grep -q` under pipefail
# (grep -q's early exit SIGPIPEs curl → a matching but large query result
# can false-negative once it outgrows the pipe buffer).
if grep -q csd-probe-line <<<"$(sc -s "http://localhost:3100/loki/api/v1/query_range?query=%7Bjob%3D%22csd%22%7D&start=$start&end=$end&limit=5" 2>/dev/null)"; then
exit 0
fi
sleep 1
done
echo "pushed log line not returned by query (loki read/write path broken)" >&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/loki@sha256:191d4fdfb726… |
| validated date | 2026-07-04 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/loki.sh |
| workload sha256 | 1d77529ff984… |
| 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: loki ready, log push (204) + query round-trip returned the line |
Recorded invocation (run_config)
The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.
| command | -config.file=/etc/loki/local-config.yaml |
|---|---|
| security_opt | no-new-privileges:true |
| mounts | loki-data:/loki |
Workload script (profiles/workloads/loki.sh)
#!/usr/bin/env bash
# Workload exerciser for the loki reference image.
#
# loki is DISTROLESS (no shell/curl), so it is probed from a SIDECAR container that
# shares loki's network namespace (`docker run --network container:<target>`) and
# reaches it on localhost:3100 — rather than `docker exec`, which has nothing to run
# inside a distroless image. The probe image is a pinned, multi-arch curl.
#
# Drives loki's real read+write path under a read-only rootfs (with /loki writable —
# the persistent data volume): push a log line and query it back. Returns 0 on
# success.
#
# Required env: LOKICONTAINER (target container name or id).
set -euo pipefail
: "${LOKICONTAINER:?LOKICONTAINER must be set}"
C="${LOKICONTAINER}"
# curlimages/curl:8.11.1 — multi-arch, digest-pinned (test-only probe image).
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 60))
until [ "$(sc -s http://localhost:3100/ready 2>/dev/null)" = ready ]; do
if (( SECONDS >= deadline )); then
echo "loki /ready never reported ready in 60s" >&2
exit 1
fi
sleep 2
done
ts="$(date +%s%N)"
code="$(sc -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' \
--data "{\"streams\":[{\"stream\":{\"job\":\"csd\"},\"values\":[[\"$ts\",\"csd-probe-line\"]]}]}" \
http://localhost:3100/loki/api/v1/push 2>/dev/null)"
if [ "$code" != 204 ]; then
echo "loki push returned HTTP $code (expected 204)" >&2
exit 1
fi
start=$((ts - 300000000000)); end=$((ts + 300000000000))
for _ in 1 2 3 4 5; do
# capture-then-match via herestring — never `sc | grep -q` under pipefail
# (grep -q's early exit SIGPIPEs curl → a matching but large query result
# can false-negative once it outgrows the pipe buffer).
if grep -q csd-probe-line <<<"$(sc -s "http://localhost:3100/loki/api/v1/query_range?query=%7Bjob%3D%22csd%22%7D&start=$start&end=$end&limit=5" 2>/dev/null)"; then
exit 0
fi
sleep 1
done
echo "pushed log line not returned by query (loki read/write path broken)" >&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
# loki — capabilities AND read-only-filesystem, derived by drop-test.
#
# capabilities: cap_drop:[ALL] + cap_add:[] — ZERO-cap. loki is a non-root
# distroless service (uid 10001) on the unprivileged :3100 writing only to its
# /loki data store, so no capability is load-bearing. Published explicitly so
# a user of the stock image (which has Docker's default 14 caps) sees the
# 14 -> 0 reduction, not just the filesystem dimension.
#
# filesystem: read_only:true, tmpfs:[] — /loki is a PERSISTENT VOLUME (NOT
# tmpfs); the derived tmpfs minimum is EMPTY.
#
# filesystem: read_only:true, tmpfs:[]. Under a read-only rootfs with only its /loki
# data dir writable, loki initialises all modules, becomes ready, ingests a pushed
# log, and serves it back on query — with a read-only /tmp (drop-tested NOT
# required). /loki holds chunks + WAL + index + rules and is a PERSISTENT VOLUME
# (must survive restarts, NOT tmpfs).
#
# loki is DISTROLESS (no shell/curl), so it is validated from a sidecar probe
# sharing its network namespace, not `docker exec` — see the criteria doc.
schema_version: "1.5"
image: docker.io/grafana/loki
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/grafana/loki.html
applies_to:
tags: ["3.7.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/loki@sha256:191d4fdfb7264f16989f0a57f320872620a5a7c2ceeec6229212c4190ec49b86
validated_date: "2026-07-16"
duration_seconds: 30
confidence: high
workload: profiles/workloads/loki.sh
workload_sha256: "1d77529ff98497ec49c6bcbf5d7218e9b34f7f574ff88a9bf8b6e31a5195fe6e"
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: ["loki-data:/loki"]
env: []
drop_test:
checks:
- {removed: CHOWN, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: DAC_OVERRIDE, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: FSETID, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: FOWNER, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: MKNOD, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: NET_RAW, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: SETGID, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: SETUID, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: SETFCAP, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: SETPCAP, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: NET_BIND_SERVICE, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: SYS_CHROOT, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: KILL, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
- {removed: AUDIT_WRITE, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}
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/loki@sha256:191d4fdfb7264f16989f0a57f320872620a5a7c2ceeec6229212c4190ec49b86
validated_date: "2026-07-04"
duration_seconds: 40
confidence: high
workload: profiles/workloads/loki.sh
workload_sha256: "1d77529ff98497ec49c6bcbf5d7218e9b34f7f574ff88a9bf8b6e31a5195fe6e"
validated_via: [drop-test, ci-smoke]
observation_backend:
ig_version: v0.51.0
gadgets: []
run_config:
user: ""
command: ["-config.file=/etc/loki/local-config.yaml"]
entrypoint: ""
network: ""
pid: ""
devices: []
security_opt: ["no-new-privileges:true"]
mounts: ["loki-data:/loki"]
env: []
drop_test:
checks:
- {removed: /tmp, required: false, observed: "correct: loki ready, log push (204) + query round-trip returned the line"}