catalog / docker.io/prom/prometheus

docker.io/prom/prometheus

validated  pin current  tags: v3.13.1

prometheus — minimum capabilities AND read-only filesystem, derived by
drop-test against the default invocation. The catalog's first fully
ZERO-PRIVILEGE profile: cap_drop:[ALL] with NO cap_add, plus read_only:true
with NO tmpfs.

Why zero: the image runs as USER nobody (uid 65534) from the first
instruction — there is no root phase, no entrypoint chown, no privilege drop
to perform, and nothing else in the serve path needs a capability. All
writes go under --storage.tsdb.path=/prometheus (TSDB blocks, WAL) — a
declared VOLUME, shipped nobody-owned, and a PERSISTENT volume in real
deployments (docker mounts it writable even under read_only; it is never a
tmpfs candidate). /tmp derived NOT required.

Both dimensions were validated together and apply as a unit. This completes
the covered grafana/loki/alloy observability stack. See
criteria/docker.io/prom/prometheus.md.

Use it

services:
  prometheus:
    image: docker.io/prom/prometheus:v3.13.1
    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/prom/prometheus@sha256:3c42b892cf72…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/prometheus.sh
workload sha256d7482a9f3546…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
DAC_OVERRIDEremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
FSETIDremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
FOWNERremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
MKNODremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
NET_RAWremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
SETGIDremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
SETUIDremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
SETFCAPremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
SETPCAPremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
NET_BIND_SERVICEremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
SYS_CHROOTremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
KILLremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)
AUDIT_WRITEremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)

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
Workload script (profiles/workloads/prometheus.sh)
#!/usr/bin/env bash
# Workload exerciser for the prometheus reference image.
#
# Drives real function end-to-end: readiness, then the query API must show the
# default config's SELF-SCRAPE produced samples (up == 1 for localhost:9090),
# then a non-zero prometheus_tsdb_head_samples_appended_total — which
# exercises the TSDB write path under --storage.tsdb.path=/prometheus. A ready
# check alone passes before the first scrape completes, and the appended
# counter reads 0 on the scrape that first reports it, so both are polled.
#
# Probes use the in-image busybox wget; the image runs as USER nobody, so
# docker exec inherits nobody and cannot pollute a derived caps minimum.
#
# Required env: PROMETHEUSCONTAINER (target container name or id).
set -euo pipefail

: "${PROMETHEUSCONTAINER:?PROMETHEUSCONTAINER must be set}"
C="${PROMETHEUSCONTAINER}"

# All probes capture-then-match — never `producer | grep -q` under pipefail
# (grep's early exit SIGPIPEs wget and a matching response reads as failure).
deadline=$((SECONDS + 45))
until grep -qi ready <<<"$(docker exec "$C" wget -qO- http://localhost:9090/-/ready 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "prometheus never ready" >&2; exit 1; }
    sleep 1
done

deadline=$((SECONDS + 45))
until grep -q '"value":\[[0-9.]*,"1"\]' <<<"$(docker exec "$C" wget -qO- 'http://localhost:9090/api/v1/query?query=up' 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "self-scrape never produced up==1" >&2; exit 1; }
    sleep 2
done

deadline=$((SECONDS + 60))
until grep -qE '"value":\[[0-9.]*,"[1-9][0-9]*"' <<<"$(docker exec "$C" wget -qO- 'http://localhost:9090/api/v1/query?query=prometheus_tsdb_head_samples_appended_total' 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "TSDB never appended samples" >&2; exit 1; }
    sleep 2
done

Dimension: filesystem

read_only: true, no tmpfs

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/prom/prometheus@sha256:3c42b892cf72…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/prometheus.sh
workload sha256d7482a9f3546…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
/tmpremovablecorrect: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)

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
Workload script (profiles/workloads/prometheus.sh)
#!/usr/bin/env bash
# Workload exerciser for the prometheus reference image.
#
# Drives real function end-to-end: readiness, then the query API must show the
# default config's SELF-SCRAPE produced samples (up == 1 for localhost:9090),
# then a non-zero prometheus_tsdb_head_samples_appended_total — which
# exercises the TSDB write path under --storage.tsdb.path=/prometheus. A ready
# check alone passes before the first scrape completes, and the appended
# counter reads 0 on the scrape that first reports it, so both are polled.
#
# Probes use the in-image busybox wget; the image runs as USER nobody, so
# docker exec inherits nobody and cannot pollute a derived caps minimum.
#
# Required env: PROMETHEUSCONTAINER (target container name or id).
set -euo pipefail

: "${PROMETHEUSCONTAINER:?PROMETHEUSCONTAINER must be set}"
C="${PROMETHEUSCONTAINER}"

# All probes capture-then-match — never `producer | grep -q` under pipefail
# (grep's early exit SIGPIPEs wget and a matching response reads as failure).
deadline=$((SECONDS + 45))
until grep -qi ready <<<"$(docker exec "$C" wget -qO- http://localhost:9090/-/ready 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "prometheus never ready" >&2; exit 1; }
    sleep 1
done

deadline=$((SECONDS + 45))
until grep -q '"value":\[[0-9.]*,"1"\]' <<<"$(docker exec "$C" wget -qO- 'http://localhost:9090/api/v1/query?query=up' 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "self-scrape never produced up==1" >&2; exit 1; }
    sleep 2
done

deadline=$((SECONDS + 60))
until grep -qE '"value":\[[0-9.]*,"[1-9][0-9]*"' <<<"$(docker exec "$C" wget -qO- 'http://localhost:9090/api/v1/query?query=prometheus_tsdb_head_samples_appended_total' 2>/dev/null)"; do
    (( SECONDS >= deadline )) && { echo "TSDB never appended samples" >&2; exit 1; }
    sleep 2
done

Evidence & provenance

Raw profile YAML
# prometheus — minimum capabilities AND read-only filesystem, derived by
# drop-test against the default invocation. The catalog's first fully
# ZERO-PRIVILEGE profile: cap_drop:[ALL] with NO cap_add, plus read_only:true
# with NO tmpfs.
#
# Why zero: the image runs as USER nobody (uid 65534) from the first
# instruction — there is no root phase, no entrypoint chown, no privilege drop
# to perform, and nothing else in the serve path needs a capability. All
# writes go under --storage.tsdb.path=/prometheus (TSDB blocks, WAL) — a
# declared VOLUME, shipped nobody-owned, and a PERSISTENT volume in real
# deployments (docker mounts it writable even under read_only; it is never a
# tmpfs candidate). /tmp derived NOT required.
#
# Both dimensions were validated together and apply as a unit. This completes
# the covered grafana/loki/alloy observability stack. See
# criteria/docker.io/prom/prometheus.md.
schema_version: "1.5"
image: docker.io/prom/prometheus
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/prom/prometheus.html
applies_to:
  tags: ["v3.13.1"]
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/prom/prometheus@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893
      validated_date: "2026-07-16"
      duration_seconds: 900
      confidence: high
      workload: profiles/workloads/prometheus.sh
      workload_sha256: "d7482a9f3546c0520ec8fb322abeba3135e6b953b36aa62e19df321d6b544d27"
      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: []
        env: []
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: DAC_OVERRIDE, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: FSETID, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: FOWNER, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: MKNOD, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: NET_RAW, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: SETGID, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: SETUID, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: SETFCAP, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: SETPCAP, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: NET_BIND_SERVICE, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: KILL, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}
  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/prom/prometheus@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893
      validated_date: "2026-07-16"
      duration_seconds: 120
      confidence: high
      workload: profiles/workloads/prometheus.sh
      workload_sha256: "d7482a9f3546c0520ec8fb322abeba3135e6b953b36aa62e19df321d6b544d27"
      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: []
        env: []
      drop_test:
        checks:
          - {removed: /tmp, required: false, observed: "correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root)"}