catalog / docker.io/netdata/netdata

docker.io/netdata/netdata

validated  pin current  tags: v2.10.3

netdata capability minimum — derived by drop-test (drop-a-cap / restart /
verify), the authoritative source for cap_add. drop-test covers the full
container lifetime, so it captures the startup-only caps SETUID/SETGID (the
root->netdata privilege drop) that runtime observation records as "unused."
Dropping those leaves netdata healthy while silently running as root.

SYS_ADMIN is removable — the image ships no ebpf.plugin, so ebpf=yes is a no-op.
It is removable EVEN for per-container network-interface metrics: netdata's
cgroup-network helper prefers setns into each container's netns (which needs
SYS_ADMIN and fails without it, once, non-fatally), then FALLS BACK to a
host-side veth method that needs no privilege — verified on the live deploy
(per-container net charts collect live data with no SYS_ADMIN). Only cosmetic:
interfaces labelled by host veth name, not in-container eth0. See criteria.
SYS_PTRACE is REQUIRED: apps.plugin runs real-uid
= the netdata user (setuid gives euid 0, not real uid 0), so it needs
CAP_SYS_PTRACE to read ANY process's /proc/<pid>/io|fd; without it it collects
every per-process metric as 0 (not null). The correctness check requires a
per-process fds_open to be NON-ZERO, so dropping SYS_PTRACE fails it. (A prior
non-null check passed on all-zeros and wrongly derived 5->3, regressing
production before this correction.)

filesystem: read_only:true, tmpfs:[/tmp]. Derived by drop-test UNDER the
capabilities dimension's recommendation (cap_drop:ALL + the 4 caps above, via
the csd spec's run.cap_add — the caps mirror of deriving caps under the fs
recommendation): the correctness check needs per-process metrics, which need
SYS_PTRACE granted. Persistent state (/etc/netdata config, /var/lib/netdata
state, /var/cache/netdata dbengine) belongs on named volumes (the official
compose shape), supplied in the derivation as service-owned (uid=201) stand-ins
— the caps minimum has no CHOWN, so a fresh root-owned dir is a staging
artifact, not a read-only finding; a prior deploy's volumes are netdata-owned.
/tmp is REQUIRED (the runtime-dir fallback when /run is unwritable — without it
the daemon exits "Cannot get/create a run directory"). /var/log/netdata derives
REMOVABLE: container logging goes to stdout; the startup chown of the log dir
fails non-fatally. READ-ONLY CONSEQUENCE: the entrypoint's PGID-based
`groupadd docker` needs a writable /etc/group and fails under read_only — grant
docker.sock access via compose `group_add:` instead of the PGID env. See
criteria.

Use it

services:
  netdata:
    image: docker.io/netdata/netdata:v2.10.3
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, SETGID, SETUID, SYS_PTRACE]
    security_opt: ["apparmor=unconfined"]
    read_only: true
    tmpfs:
      - /tmp

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 + cap_add: DAC_OVERRIDE, SETGID, SETUID, SYS_PTRACE

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

Feature coverage (ledger)

A drop-test proves the minimum for what the workload exercised. Privilege-relevant features it did not drive are listed honestly — using one may need more than this minimum.

featureevidence / reason
drivenhost + per-process monitoring (apps.plugin /proc io/fd metrics)the correctness check requires a per-process fds_open chart to be NON-ZERO (dropping SYS_PTRACE zeroes it)
drivenper-container network-interface metrics (cgroup-network)verified on the live deploy: collects via the host-side veth fallback under the derived set (SYS_ADMIN's setns path fails once, non-fatally)
not driveneBPF collectors (ebpf.plugin)the image ships no ebpf.plugin, so nothing gates on it (ebpf=yes is a no-op); a custom build with the plugin would need re-derivation
not drivendocker.sock-based collectors (container names/state)socket access is a group grant, not a capability, so it cannot move this dimension; per-process docker metrics come via /proc + SYS_PTRACE
not drivenremaining optional collector surfacenot exhaustively driven — the reason this dimension's coverage grades partial (ADR-018); no known collector requires a cap outside the derived set

Drop-test evidence

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

RemovedVerdictObserved
SYS_PTRACErequiredper-process metrics all zero — apps.plugin can't read /proc/<pid>/fd (SYS_PTRACE gap)
SYS_ADMINremovablecorrect: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)
DAC_OVERRIDErequirednot ready (status=exited): API/apps.plugin not up
CHOWNremovablecorrect: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)
FOWNERremovablecorrect: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)
SETUIDrequireddaemon running as ROOT (privilege drop failed)
SETGIDrequireddaemon running as ROOT (privilege drop failed)

Recorded invocation (run_config)

The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.

networkhost
pidhost
security_optapparmor=unconfined
mounts/proc:/host/proc:ro
/sys:/host/sys:ro
/etc/os-release:/host/etc/os-release:ro
/var/log:/host/var/log:ro
/var/run/docker.sock:/var/run/docker.sock:ro
/etc/passwd:/host/etc/passwd:ro
/etc/group:/host/etc/group:ro
envPGID
Workload script (profiles/workloads/netdata.sh)
#!/usr/bin/env bash
# Correctness check for netdata cap_add bisection (docker.io/netdata/netdata).
#
# netdata is run as a host monitor — pid:host, host /proc,/sys,/var/run/docker.sock
# read-only mounts, host network, and the granted cap set present — then each
# candidate capability is dropped, the container restarted, and this correctness check
# decides "still works CORRECTLY". Correctness is not just liveness: netdata must
# still drop root -> the unprivileged `netdata` user AND still collect per-process
# metrics for non-root processes, or the derivation is wrong.
#
# Required env: NDCONTAINER (target container name or id).
# Exit 0 = correct; non-zero = the dropped capability was required.
set -euo pipefail
: "${NDCONTAINER:?NDCONTAINER must be set}"

API=http://localhost:19999
# chart_nonzero <chart-id> -> 0 if the latest sample has a NON-ZERO value.
# Non-zero, not merely non-null: apps.plugin denied access to a process's /proc
# reports the metric as 0 (not null), so a non-null check passes on broken
# collection. A live process always has open fds > 0.
chart_nonzero() {
    curl -fsS --max-time 4 "${API}/api/v1/data?chart=$1&after=-20&points=1&format=json" 2>/dev/null \
        | python3 -c 'import sys,json
try:
 rows=json.load(sys.stdin).get("data",[])
 sys.exit(0 if [v for r in rows for v in r[1:] if v is not None and v != 0] else 1)
except Exception:
 sys.exit(1)'
}

# 1. Healthy within the container's own healthcheck window.
deadline=$((SECONDS + 75))
until [ "$(docker inspect -f '{{.State.Health.Status}}' "$NDCONTAINER" 2>/dev/null)" = healthy ]; do
    [ "$(docker inspect -f '{{.State.Status}}' "$NDCONTAINER" 2>/dev/null)" != running ] && { echo "not running"; exit 1; }
    (( SECONDS >= deadline )) && { echo "did not become healthy"; exit 1; }
    sleep 3
done

# 2. HTTP API up and apps.plugin collecting.
curl -fsS --max-time 5 "${API}/api/v1/info" >/dev/null || { echo "API down"; exit 1; }
# capture then grep — piping the (large) charts dump into `grep -q` can SIGPIPE
# curl and, under pipefail, mark the pipeline failed despite a match.
charts="$(curl -fsS --max-time 5 "${API}/api/v1/charts")" || { echo "charts API down"; exit 1; }
grep -q '"apps\.' <<<"$charts" || { echo "apps.plugin not collecting"; exit 1; }

# 3. CORRECTNESS: the netdata daemon dropped privileges (must NOT be uid 0/root).
pid="$(docker inspect -f '{{.State.Pid}}' "$NDCONTAINER")"
uid="$(awk '/^Uid:/{print $2}' /proc/"$pid"/status)"
[ "$uid" != 0 ] || { echo "daemon is running as ROOT (privilege drop failed)"; exit 1; }

# 4. CORRECTNESS: per-process io/fd metrics are actually COLLECTED (non-zero).
# apps.plugin runs real-uid = the netdata user (setuid gives euid 0, not real uid
# 0), so it needs CAP_SYS_PTRACE to read ANY process's /proc/<pid>/io|fd; without
# it, it reports every per-process metric as 0 (not null). So require a per-process
# fds_open to be NON-ZERO — dropping SYS_PTRACE zeros it. (A prior non-null check
# passed on all-zeros and wrongly derived SYS_PTRACE removable, regressing prod.)
ppok=0
for _ in 1 2 3 4 5 6 7 8; do
    for ch in app.dockerd_fds_open app.netdata_fds_open app.systemd-journald_fds_open app.containerd_fds_open; do
        if chart_nonzero "$ch"; then ppok=1; PPCH="$ch"; break 2; fi
    done
    sleep 3
done
[ "$ppok" = 1 ] || { echo "per-process metrics all zero — apps.plugin can't read /proc/<pid>/fd (SYS_PTRACE gap)"; exit 1; }

echo "netdata correct: healthy, API up, daemon uid=$uid (non-root), per-process metrics collecting non-zero ($PPCH)"

Dimension: filesystem

read_only: true + tmpfs: /tmp

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/netdata/netdata@sha256:bcc822ec685d…
validated date2026-07-17
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/netdata.sh
workload sha256ba6ae07c95ee…
ig versionv0.51.0

Feature coverage (ledger)

A drop-test proves the minimum for what the workload exercised. Privilege-relevant features it did not drive are listed honestly — using one may need more than this minimum.

featureevidence / reason
drivenhost + per-process monitoring under read-only rootfssame correctness predicate as the caps derivation, healthy end-to-end with rootfs read-only
not drivenfile-based logging ([logs] method = file)the container default logs to stdout (why /var/log/netdata derives removable); a file-logging config diverges and needs a writable log path
not drivencloud claiming / dyncfg persistent writesland in /var/lib/netdata, which is a named volume in production (a stand-in here), so they cannot move the tmpfs minimum

Drop-test evidence

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

RemovedVerdictObserved
/var/log/netdataremovablecorrect: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)
/tmprequirednot ready (status=exited): API/apps.plugin not up

Recorded invocation (run_config)

The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.

networkhost
pidhost
security_optapparmor=unconfined
mounts/proc:/host/proc:ro
/sys:/host/sys:ro
/etc/os-release:/host/etc/os-release:ro
/var/log:/host/var/log:ro
/var/run/docker.sock:/var/run/docker.sock:ro
/etc/passwd:/host/etc/passwd:ro
/etc/group:/host/etc/group:ro
envPGID
Workload script (profiles/workloads/netdata.sh)
#!/usr/bin/env bash
# Correctness check for netdata cap_add bisection (docker.io/netdata/netdata).
#
# netdata is run as a host monitor — pid:host, host /proc,/sys,/var/run/docker.sock
# read-only mounts, host network, and the granted cap set present — then each
# candidate capability is dropped, the container restarted, and this correctness check
# decides "still works CORRECTLY". Correctness is not just liveness: netdata must
# still drop root -> the unprivileged `netdata` user AND still collect per-process
# metrics for non-root processes, or the derivation is wrong.
#
# Required env: NDCONTAINER (target container name or id).
# Exit 0 = correct; non-zero = the dropped capability was required.
set -euo pipefail
: "${NDCONTAINER:?NDCONTAINER must be set}"

API=http://localhost:19999
# chart_nonzero <chart-id> -> 0 if the latest sample has a NON-ZERO value.
# Non-zero, not merely non-null: apps.plugin denied access to a process's /proc
# reports the metric as 0 (not null), so a non-null check passes on broken
# collection. A live process always has open fds > 0.
chart_nonzero() {
    curl -fsS --max-time 4 "${API}/api/v1/data?chart=$1&after=-20&points=1&format=json" 2>/dev/null \
        | python3 -c 'import sys,json
try:
 rows=json.load(sys.stdin).get("data",[])
 sys.exit(0 if [v for r in rows for v in r[1:] if v is not None and v != 0] else 1)
except Exception:
 sys.exit(1)'
}

# 1. Healthy within the container's own healthcheck window.
deadline=$((SECONDS + 75))
until [ "$(docker inspect -f '{{.State.Health.Status}}' "$NDCONTAINER" 2>/dev/null)" = healthy ]; do
    [ "$(docker inspect -f '{{.State.Status}}' "$NDCONTAINER" 2>/dev/null)" != running ] && { echo "not running"; exit 1; }
    (( SECONDS >= deadline )) && { echo "did not become healthy"; exit 1; }
    sleep 3
done

# 2. HTTP API up and apps.plugin collecting.
curl -fsS --max-time 5 "${API}/api/v1/info" >/dev/null || { echo "API down"; exit 1; }
# capture then grep — piping the (large) charts dump into `grep -q` can SIGPIPE
# curl and, under pipefail, mark the pipeline failed despite a match.
charts="$(curl -fsS --max-time 5 "${API}/api/v1/charts")" || { echo "charts API down"; exit 1; }
grep -q '"apps\.' <<<"$charts" || { echo "apps.plugin not collecting"; exit 1; }

# 3. CORRECTNESS: the netdata daemon dropped privileges (must NOT be uid 0/root).
pid="$(docker inspect -f '{{.State.Pid}}' "$NDCONTAINER")"
uid="$(awk '/^Uid:/{print $2}' /proc/"$pid"/status)"
[ "$uid" != 0 ] || { echo "daemon is running as ROOT (privilege drop failed)"; exit 1; }

# 4. CORRECTNESS: per-process io/fd metrics are actually COLLECTED (non-zero).
# apps.plugin runs real-uid = the netdata user (setuid gives euid 0, not real uid
# 0), so it needs CAP_SYS_PTRACE to read ANY process's /proc/<pid>/io|fd; without
# it, it reports every per-process metric as 0 (not null). So require a per-process
# fds_open to be NON-ZERO — dropping SYS_PTRACE zeros it. (A prior non-null check
# passed on all-zeros and wrongly derived SYS_PTRACE removable, regressing prod.)
ppok=0
for _ in 1 2 3 4 5 6 7 8; do
    for ch in app.dockerd_fds_open app.netdata_fds_open app.systemd-journald_fds_open app.containerd_fds_open; do
        if chart_nonzero "$ch"; then ppok=1; PPCH="$ch"; break 2; fi
    done
    sleep 3
done
[ "$ppok" = 1 ] || { echo "per-process metrics all zero — apps.plugin can't read /proc/<pid>/fd (SYS_PTRACE gap)"; exit 1; }

echo "netdata correct: healthy, API up, daemon uid=$uid (non-root), per-process metrics collecting non-zero ($PPCH)"

Evidence & provenance

Raw profile YAML
# netdata capability minimum — derived by drop-test (drop-a-cap / restart /
# verify), the authoritative source for cap_add. drop-test covers the full
# container lifetime, so it captures the startup-only caps SETUID/SETGID (the
# root->netdata privilege drop) that runtime observation records as "unused."
# Dropping those leaves netdata healthy while silently running as root.
#
# SYS_ADMIN is removable — the image ships no ebpf.plugin, so ebpf=yes is a no-op.
# It is removable EVEN for per-container network-interface metrics: netdata's
# cgroup-network helper prefers setns into each container's netns (which needs
# SYS_ADMIN and fails without it, once, non-fatally), then FALLS BACK to a
# host-side veth method that needs no privilege — verified on the live deploy
# (per-container net charts collect live data with no SYS_ADMIN). Only cosmetic:
# interfaces labelled by host veth name, not in-container eth0. See criteria.
# SYS_PTRACE is REQUIRED: apps.plugin runs real-uid
# = the netdata user (setuid gives euid 0, not real uid 0), so it needs
# CAP_SYS_PTRACE to read ANY process's /proc/<pid>/io|fd; without it it collects
# every per-process metric as 0 (not null). The correctness check requires a
# per-process fds_open to be NON-ZERO, so dropping SYS_PTRACE fails it. (A prior
# non-null check passed on all-zeros and wrongly derived 5->3, regressing
# production before this correction.)
#
# filesystem: read_only:true, tmpfs:[/tmp]. Derived by drop-test UNDER the
# capabilities dimension's recommendation (cap_drop:ALL + the 4 caps above, via
# the csd spec's run.cap_add — the caps mirror of deriving caps under the fs
# recommendation): the correctness check needs per-process metrics, which need
# SYS_PTRACE granted. Persistent state (/etc/netdata config, /var/lib/netdata
# state, /var/cache/netdata dbengine) belongs on named volumes (the official
# compose shape), supplied in the derivation as service-owned (uid=201) stand-ins
# — the caps minimum has no CHOWN, so a fresh root-owned dir is a staging
# artifact, not a read-only finding; a prior deploy's volumes are netdata-owned.
# /tmp is REQUIRED (the runtime-dir fallback when /run is unwritable — without it
# the daemon exits "Cannot get/create a run directory"). /var/log/netdata derives
# REMOVABLE: container logging goes to stdout; the startup chown of the log dir
# fails non-fatally. READ-ONLY CONSEQUENCE: the entrypoint's PGID-based
# `groupadd docker` needs a writable /etc/group and fails under read_only — grant
# docker.sock access via compose `group_add:` instead of the PGID env. See
# criteria.
schema_version: "1.6"
image: docker.io/netdata/netdata
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/netdata/netdata.html
applies_to:
  tags: ["v2.10.3"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, SETGID, SETUID, SYS_PTRACE]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/netdata/netdata@sha256:bcc822ec685dd7ee488e9f99b957f479b66301be04f13f346df692ad7de94151
      validated_date: "2026-07-04"
      duration_seconds: 900
      confidence: moderate
      workload: profiles/workloads/netdata.sh
      workload_sha256: ba6ae07c95eed30155baff45933b0ba9c171694a294539b08119360fc31180f6
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: host
        pid: host
        devices: []
        security_opt: [apparmor=unconfined]
        mounts:
          - /proc:/host/proc:ro
          - /sys:/host/sys:ro
          - /etc/os-release:/host/etc/os-release:ro
          - /var/log:/host/var/log:ro
          - /var/run/docker.sock:/var/run/docker.sock:ro
          - /etc/passwd:/host/etc/passwd:ro
          - /etc/group:/host/etc/group:ro
        env: [PGID]
      features:
        - name: "host + per-process monitoring (apps.plugin /proc io/fd metrics)"
          driven: true
          why: "the correctness check requires a per-process fds_open chart to be NON-ZERO (dropping SYS_PTRACE zeroes it)"
        - name: "per-container network-interface metrics (cgroup-network)"
          driven: true
          why: "verified on the live deploy: collects via the host-side veth fallback under the derived set (SYS_ADMIN's setns path fails once, non-fatally)"
        - name: "eBPF collectors (ebpf.plugin)"
          driven: false
          why: "the image ships no ebpf.plugin, so nothing gates on it (ebpf=yes is a no-op); a custom build with the plugin would need re-derivation"
        - name: "docker.sock-based collectors (container names/state)"
          driven: false
          why: "socket access is a group grant, not a capability, so it cannot move this dimension; per-process docker metrics come via /proc + SYS_PTRACE"
        - name: "remaining optional collector surface"
          driven: false
          why: "not exhaustively driven — the reason this dimension's coverage grades partial (ADR-018); no known collector requires a cap outside the derived set"
      drop_test:
        checks:
          - removed: SYS_PTRACE
            required: true
            observed: "per-process metrics all zero — apps.plugin can't read /proc/<pid>/fd (SYS_PTRACE gap)"
          - removed: SYS_ADMIN
            required: false
            observed: "correct: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)"
          - removed: DAC_OVERRIDE
            required: true
            observed: "not ready (status=exited): API/apps.plugin not up"
          - removed: CHOWN
            required: false
            observed: "correct: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)"
          - removed: FOWNER
            required: false
            observed: "correct: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)"
          - removed: SETUID
            required: true
            observed: "daemon running as ROOT (privilege drop failed)"
          - removed: SETGID
            required: true
            observed: "daemon running as ROOT (privilege drop failed)"
  filesystem:
    read_only: true
    tmpfs: [/tmp]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/netdata/netdata@sha256:bcc822ec685dd7ee488e9f99b957f479b66301be04f13f346df692ad7de94151
      validated_date: "2026-07-17"
      duration_seconds: 36
      confidence: high
      workload: profiles/workloads/netdata.sh
      workload_sha256: ba6ae07c95eed30155baff45933b0ba9c171694a294539b08119360fc31180f6
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: host
        pid: host
        devices: []
        security_opt: [apparmor=unconfined]
        mounts:
          - /proc:/host/proc:ro
          - /sys:/host/sys:ro
          - /etc/os-release:/host/etc/os-release:ro
          - /var/log:/host/var/log:ro
          - /var/run/docker.sock:/var/run/docker.sock:ro
          - /etc/passwd:/host/etc/passwd:ro
          - /etc/group:/host/etc/group:ro
        env: [PGID]
      features:
        - name: "host + per-process monitoring under read-only rootfs"
          driven: true
          why: "same correctness predicate as the caps derivation, healthy end-to-end with rootfs read-only"
        - name: "file-based logging ([logs] method = file)"
          driven: false
          why: "the container default logs to stdout (why /var/log/netdata derives removable); a file-logging config diverges and needs a writable log path"
        - name: "cloud claiming / dyncfg persistent writes"
          driven: false
          why: "land in /var/lib/netdata, which is a named volume in production (a stand-in here), so they cannot move the tmpfs minimum"
      drop_test:
        checks:
          - removed: /var/log/netdata
            required: false
            observed: "correct: ready, daemon uid=201, per-process metrics collecting non-zero (app.dockerd_fds_open)"
          - removed: /tmp
            required: true
            observed: "not ready (status=exited): API/apps.plugin not up"