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)
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/prom/prometheus@sha256:3c42b892cf72… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/prometheus.sh |
| workload sha256 | d7482a9f3546… |
| 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: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
DAC_OVERRIDE | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
FSETID | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
FOWNER | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
MKNOD | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
NET_RAW | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
SETGID | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
SETUID | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
SETFCAP | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
SETPCAP | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
NET_BIND_SERVICE | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
SYS_CHROOT | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
KILL | removable | correct: self-scrape up==1, TSDB appending samples, prometheus uid=65534 (non-root) |
AUDIT_WRITE | removable | correct: 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_opt | no-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
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/prom/prometheus@sha256:3c42b892cf72… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/prometheus.sh |
| workload sha256 | d7482a9f3546… |
| 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: 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_opt | no-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
- 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
# 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)"}