catalog / docker.io/library/traefik

docker.io/library/traefik

validated  pin current  tags: v3.7

traefik — minimum Linux capabilities, derived by drop-test against the
file-provider reverse-proxy invocation (static config via flags, one routed
upstream). This profile trims the full Docker default cap set 14 -> 1.

capabilities: cap_drop:[ALL] + cap_add:[NET_BIND_SERVICE]. traefik runs as
root with NO privilege drop; the one load-bearing capability is binding
entrypoint :80 — dropping it exits at startup ("listen tcp :80: bind:
permission denied"). IMPORTANT SCOPE: that requirement exists only under the
hardened kernel posture net.ipv4.ip_unprivileged_port_start=1024, which the
derivation PINS via sysctl (docker's default is 0 = all ports unprivileged,
where the cap would read falsely-removable). Deployments keeping docker's
default sysctl can drop even this cap; deployments with high-port
entrypoints (:8080) need no cap either.

THE DOCKER-PROVIDER VARIANT IS OUT OF SCOPE: mounting /var/run/docker.sock
makes the socket itself the dominant risk (root-equivalent host control) —
a capability minimum CANNOT address it and must not be read as making that
deployment safe. This profile covers the file-provider shape only. See
criteria/docker.io/library/traefik.md.
filesystem: read_only:true, tmpfs:[]. traefik writes nothing to its rootfs
when proxying — it runs correctly under a read-only rootfs with no tmpfs. Its
dynamic routing config rides a READ-ONLY mount (the file provider reads it,
never writes); /tmp was drop-tested and is NOT required. Derived by drop-test.

Use it

services:
  traefik:
    image: docker.io/library/traefik:v3.7
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    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 + cap_add: NET_BIND_SERVICE

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/library/traefik@sha256:4299bbed8504…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/traefik.sh
workload sha2563e5d93cd16b4…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: :80 routed through traefik to the caddy backend
DAC_OVERRIDEremovablecorrect: :80 routed through traefik to the caddy backend
FSETIDremovablecorrect: :80 routed through traefik to the caddy backend
FOWNERremovablecorrect: :80 routed through traefik to the caddy backend
MKNODremovablecorrect: :80 routed through traefik to the caddy backend
NET_RAWremovablecorrect: :80 routed through traefik to the caddy backend
SETGIDremovablecorrect: :80 routed through traefik to the caddy backend
SETUIDremovablecorrect: :80 routed through traefik to the caddy backend
SETFCAPremovablecorrect: :80 routed through traefik to the caddy backend
SETPCAPremovablecorrect: :80 routed through traefik to the caddy backend
NET_BIND_SERVICErequiredcontainer exited: ERR Command error: error while building entryPoint web: building listener: error opening listener: listen tcp :80: bind: permission denied
SYS_CHROOTremovablecorrect: :80 routed through traefik to the caddy backend
KILLremovablecorrect: :80 routed through traefik to the caddy backend
AUDIT_WRITEremovablecorrect: :80 routed through traefik to the caddy backend

Recorded invocation (run_config)

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

command--entrypoints.web.address=:80
--providers.file.directory=/tmp
--providers.file.watch=true
--api=false
--log.level=INFO
security_optno-new-privileges:true
sysctlsnet.ipv4.ip_unprivileged_port_start=1024
Workload script (profiles/workloads/traefik.sh)
#!/usr/bin/env bash
# Workload exerciser for the traefik reference image (file-provider
# reverse-proxy invocation).
#
# Drives traefik's real function end-to-end: a request on entrypoint :80 must
# be routed through a traefik router to an upstream named `backend` (in the
# derivation stack, a pinned caddy on the shared network) and answered with
# the backend's content. The dynamic file-provider config is docker-cp'd into
# /tmp (traefik is started with --providers.file.directory=/tmp watch=true and
# hot-loads it) — a daemon-side write that exercises no capability inside the
# target. Probes run from a curl sidecar sharing the target's netns.
#
# Required env: TRAEFIKCONTAINER (target container name or id). Requires an
# HTTP responder reachable from the target as http://backend:80.
set -euo pipefail

: "${TRAEFIKCONTAINER:?TRAEFIKCONTAINER must be set}"
C="${TRAEFIKCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/dynamic.yml" <<'EOF'
http:
  routers:
    csd:
      rule: PathPrefix(`/`)
      service: backend
  services:
    backend:
      loadBalancer:
        servers:
          - url: http://backend:80
EOF
docker cp "$tmp/dynamic.yml" "$C:/tmp/dynamic.yml" >/dev/null

deadline=$((SECONDS + 45))
while :; do
    body="$(sc -s --max-time 5 http://localhost:80/ 2>/dev/null)"
    case "$body" in *[Cc]addy*|*backend*) break;; esac
    (( SECONDS >= deadline )) && { echo "no routed backend response" >&2; exit 1; }
    sleep 2
done

Dimension: filesystem

read_only: true, no tmpfs

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/library/traefik@sha256:4299bbed8504…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/traefik.sh
workload sha2563e5d93cd16b4…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
/tmpremovablecorrect: :80 routed through traefik to the caddy backend (read-only rootfs)

Recorded invocation (run_config)

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

command--entrypoints.web.address=:80
--providers.file.directory=/etc/traefik/dynamic
--providers.file.watch=false
--api=false
--log.level=INFO
Workload script (profiles/workloads/traefik.sh)
#!/usr/bin/env bash
# Workload exerciser for the traefik reference image (file-provider
# reverse-proxy invocation).
#
# Drives traefik's real function end-to-end: a request on entrypoint :80 must
# be routed through a traefik router to an upstream named `backend` (in the
# derivation stack, a pinned caddy on the shared network) and answered with
# the backend's content. The dynamic file-provider config is docker-cp'd into
# /tmp (traefik is started with --providers.file.directory=/tmp watch=true and
# hot-loads it) — a daemon-side write that exercises no capability inside the
# target. Probes run from a curl sidecar sharing the target's netns.
#
# Required env: TRAEFIKCONTAINER (target container name or id). Requires an
# HTTP responder reachable from the target as http://backend:80.
set -euo pipefail

: "${TRAEFIKCONTAINER:?TRAEFIKCONTAINER must be set}"
C="${TRAEFIKCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/dynamic.yml" <<'EOF'
http:
  routers:
    csd:
      rule: PathPrefix(`/`)
      service: backend
  services:
    backend:
      loadBalancer:
        servers:
          - url: http://backend:80
EOF
docker cp "$tmp/dynamic.yml" "$C:/tmp/dynamic.yml" >/dev/null

deadline=$((SECONDS + 45))
while :; do
    body="$(sc -s --max-time 5 http://localhost:80/ 2>/dev/null)"
    case "$body" in *[Cc]addy*|*backend*) break;; esac
    (( SECONDS >= deadline )) && { echo "no routed backend response" >&2; exit 1; }
    sleep 2
done

Evidence & provenance

Raw profile YAML
# traefik — minimum Linux capabilities, derived by drop-test against the
# file-provider reverse-proxy invocation (static config via flags, one routed
# upstream). This profile trims the full Docker default cap set 14 -> 1.
#
# capabilities: cap_drop:[ALL] + cap_add:[NET_BIND_SERVICE]. traefik runs as
# root with NO privilege drop; the one load-bearing capability is binding
# entrypoint :80 — dropping it exits at startup ("listen tcp :80: bind:
# permission denied"). IMPORTANT SCOPE: that requirement exists only under the
# hardened kernel posture net.ipv4.ip_unprivileged_port_start=1024, which the
# derivation PINS via sysctl (docker's default is 0 = all ports unprivileged,
# where the cap would read falsely-removable). Deployments keeping docker's
# default sysctl can drop even this cap; deployments with high-port
# entrypoints (:8080) need no cap either.
#
# THE DOCKER-PROVIDER VARIANT IS OUT OF SCOPE: mounting /var/run/docker.sock
# makes the socket itself the dominant risk (root-equivalent host control) —
# a capability minimum CANNOT address it and must not be read as making that
# deployment safe. This profile covers the file-provider shape only. See
# criteria/docker.io/library/traefik.md.
# filesystem: read_only:true, tmpfs:[]. traefik writes nothing to its rootfs
# when proxying — it runs correctly under a read-only rootfs with no tmpfs. Its
# dynamic routing config rides a READ-ONLY mount (the file provider reads it,
# never writes); /tmp was drop-tested and is NOT required. Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/library/traefik
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/traefik.html
applies_to:
  tags: ["v3.7"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/traefik@sha256:4299bbed850421258fc5448c2e0e6ad350981d4d335a68de11b92448aedbefe5
      validated_date: "2026-07-16"
      duration_seconds: 300
      confidence: high
      workload: profiles/workloads/traefik.sh
      workload_sha256: "3e5d93cd16b4a4e8f8fd731396b495fdc6d3e6603fbd30fb7945a699f58fb6f1"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: ["--entrypoints.web.address=:80", "--providers.file.directory=/tmp",
                  "--providers.file.watch=true", "--api=false", "--log.level=INFO"]
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: ["no-new-privileges:true"]
        sysctls: ["net.ipv4.ip_unprivileged_port_start=1024"]
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: DAC_OVERRIDE, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: FSETID, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: FOWNER, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: MKNOD, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: NET_RAW, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: SETGID, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: SETUID, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: SETFCAP, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: SETPCAP, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: NET_BIND_SERVICE, required: true, observed: "container exited: ERR Command error: error while building entryPoint web: building listener: error opening listener: listen tcp :80: bind: permission denied"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: KILL, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: :80 routed through traefik to the caddy backend"}
  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/library/traefik@sha256:4299bbed850421258fc5448c2e0e6ad350981d4d335a68de11b92448aedbefe5
      validated_date: "2026-07-16"
      duration_seconds: 5
      confidence: high
      workload: profiles/workloads/traefik.sh
      workload_sha256: "3e5d93cd16b4a4e8f8fd731396b495fdc6d3e6603fbd30fb7945a699f58fb6f1"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: [--entrypoints.web.address=:80, --providers.file.directory=/etc/traefik/dynamic, --providers.file.watch=false, --api=false, --log.level=INFO]
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: []
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: /tmp, required: false, observed: "correct: :80 routed through traefik to the caddy backend (read-only rootfs)"}