catalog / docker.io/grafana/loki

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)

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

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: loki ready, log push (204) + query round-trip returned the line
DAC_OVERRIDEremovablecorrect: loki ready, log push (204) + query round-trip returned the line
FSETIDremovablecorrect: loki ready, log push (204) + query round-trip returned the line
FOWNERremovablecorrect: loki ready, log push (204) + query round-trip returned the line
MKNODremovablecorrect: loki ready, log push (204) + query round-trip returned the line
NET_RAWremovablecorrect: loki ready, log push (204) + query round-trip returned the line
SETGIDremovablecorrect: loki ready, log push (204) + query round-trip returned the line
SETUIDremovablecorrect: loki ready, log push (204) + query round-trip returned the line
SETFCAPremovablecorrect: loki ready, log push (204) + query round-trip returned the line
SETPCAPremovablecorrect: loki ready, log push (204) + query round-trip returned the line
NET_BIND_SERVICEremovablecorrect: loki ready, log push (204) + query round-trip returned the line
SYS_CHROOTremovablecorrect: loki ready, log push (204) + query round-trip returned the line
KILLremovablecorrect: loki ready, log push (204) + query round-trip returned the line
AUDIT_WRITEremovablecorrect: 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_optno-new-privileges:true
mountsloki-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

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/grafana/loki@sha256:191d4fdfb726…
validated date2026-07-04
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/loki.sh
workload sha2561d77529ff984…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
/tmpremovablecorrect: 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_optno-new-privileges:true
mountsloki-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

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