catalog / docker.io/library/haproxy

docker.io/library/haproxy

validated  pin current  tags: 3.2

haproxy — minimum Linux capabilities, derived by drop-test against a
reverse-proxy invocation (frontend :8080 -> one backend). ZERO-cap:
cap_drop:[ALL] with no cap_add — and the catalog's cleanest teaching about
non-root images: haproxy runs as USER haproxy (uid 99) since 2.4, and a
non-root process in docker receives NO effective capabilities from cap_add
(docker sets no ambient caps) — so a privileged :80 bind is simply
impossible in this image; the supported pattern is an unprivileged frontend
port behind a port mapping, which needs nothing.

The image ships no default config and a committed spec cannot carry a bind
mount, so the derivation's command writes the minimal config to /tmp and
execs haproxy on it — recorded honestly in run_config. See
criteria/docker.io/library/haproxy.md.
filesystem: read_only:true, tmpfs:[]. haproxy is a static-binary proxy that
writes nothing to its rootfs — it runs correctly under a read-only rootfs with
no tmpfs. Its config rides a READ-ONLY mount at the default config path; /tmp
was drop-tested and is NOT required. Derived by drop-test.

Use it

services:
  haproxy:
    image: docker.io/library/haproxy:3.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/library/haproxy@sha256:e271912a69f3…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/haproxy.sh
workload sha2565cf2468a731b…
ig versionv0.51.0

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
DAC_OVERRIDEremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
FSETIDremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
FOWNERremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
MKNODremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
NET_RAWremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
SETGIDremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
SETUIDremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
SETFCAPremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
SETPCAPremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
NET_BIND_SERVICEremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
SYS_CHROOTremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
KILLremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
AUDIT_WRITEremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)

Recorded invocation (run_config)

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

command-c
printf <minimal-cfg> > /tmp/h.cfg && exec haproxy -f /tmp/h.cfg
security_optno-new-privileges:true
Workload script (profiles/workloads/haproxy.sh)
#!/usr/bin/env bash
# Workload exerciser for the haproxy reference image (reverse-proxy
# invocation: frontend :8080 -> an upstream named `backend`). Real function =
# a request proxied through and answered with the backend's content, plus the
# non-root assert (the image is USER haproxy by construction).
# Required env: HAPROXYCONTAINER (target container name or id). Requires an
# HTTP responder reachable from the target as http://backend:80.
set -euo pipefail
: "${HAPROXYCONTAINER:?HAPROXYCONTAINER must be set}"
C="${HAPROXYCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 30))
while :; do
    body="$(sc -s --max-time 5 http://localhost:8080/ 2>/dev/null)"
    case "$body" in *[Cc]addy*|*backend*) break;; esac
    (( SECONDS >= deadline )) && { echo "no proxied backend response" >&2; exit 1; }
    sleep 1
done
uid="$(docker exec "$C" sh -c 'awk "/^Uid:/{print \$2}" /proc/1/status')"
[ "${uid:-0}" != 0 ] || { echo "haproxy running as ROOT" >&2; exit 1; }

Dimension: filesystem

read_only: true, no tmpfs

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

Drop-test evidence

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

RemovedVerdictObserved
/tmpremovablecorrect: :8080 proxied to caddy backend, haproxy uid=99 (non-root)
Workload script (profiles/workloads/haproxy.sh)
#!/usr/bin/env bash
# Workload exerciser for the haproxy reference image (reverse-proxy
# invocation: frontend :8080 -> an upstream named `backend`). Real function =
# a request proxied through and answered with the backend's content, plus the
# non-root assert (the image is USER haproxy by construction).
# Required env: HAPROXYCONTAINER (target container name or id). Requires an
# HTTP responder reachable from the target as http://backend:80.
set -euo pipefail
: "${HAPROXYCONTAINER:?HAPROXYCONTAINER must be set}"
C="${HAPROXYCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 30))
while :; do
    body="$(sc -s --max-time 5 http://localhost:8080/ 2>/dev/null)"
    case "$body" in *[Cc]addy*|*backend*) break;; esac
    (( SECONDS >= deadline )) && { echo "no proxied backend response" >&2; exit 1; }
    sleep 1
done
uid="$(docker exec "$C" sh -c 'awk "/^Uid:/{print \$2}" /proc/1/status')"
[ "${uid:-0}" != 0 ] || { echo "haproxy running as ROOT" >&2; exit 1; }

Evidence & provenance

Raw profile YAML
# haproxy — minimum Linux capabilities, derived by drop-test against a
# reverse-proxy invocation (frontend :8080 -> one backend). ZERO-cap:
# cap_drop:[ALL] with no cap_add — and the catalog's cleanest teaching about
# non-root images: haproxy runs as USER haproxy (uid 99) since 2.4, and a
# non-root process in docker receives NO effective capabilities from cap_add
# (docker sets no ambient caps) — so a privileged :80 bind is simply
# impossible in this image; the supported pattern is an unprivileged frontend
# port behind a port mapping, which needs nothing.
#
# The image ships no default config and a committed spec cannot carry a bind
# mount, so the derivation's command writes the minimal config to /tmp and
# execs haproxy on it — recorded honestly in run_config. See
# criteria/docker.io/library/haproxy.md.
# filesystem: read_only:true, tmpfs:[]. haproxy is a static-binary proxy that
# writes nothing to its rootfs — it runs correctly under a read-only rootfs with
# no tmpfs. Its config rides a READ-ONLY mount at the default config path; /tmp
# was drop-tested and is NOT required. Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/library/haproxy
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/haproxy.html
applies_to:
  tags: ["3.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/library/haproxy@sha256:e271912a69f3114b12aeea3362e32ae02bc8d1e330d748e06da3ae6c0522f73c
      validated_date: "2026-07-16"
      duration_seconds: 240
      confidence: high
      workload: profiles/workloads/haproxy.sh
      workload_sha256: "5cf2468a731b284b8da4999176b8981f25c7d17f2de84322900a70d1f12f7e23"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: ["-c", "printf <minimal-cfg> > /tmp/h.cfg && exec haproxy -f /tmp/h.cfg"]
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: ["no-new-privileges:true"]
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: DAC_OVERRIDE, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: FSETID, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: FOWNER, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: MKNOD, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: NET_RAW, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: SETGID, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: SETUID, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: SETFCAP, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: SETPCAP, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: NET_BIND_SERVICE, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: KILL, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (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/library/haproxy@sha256:e271912a69f3114b12aeea3362e32ae02bc8d1e330d748e06da3ae6c0522f73c
      validated_date: "2026-07-16"
      duration_seconds: 4
      confidence: high
      workload: profiles/workloads/haproxy.sh
      workload_sha256: "5cf2468a731b284b8da4999176b8981f25c7d17f2de84322900a70d1f12f7e23"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: []
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: /tmp, required: false, observed: "correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root)"}