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)
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/haproxy@sha256:e271912a69f3… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/haproxy.sh |
| workload sha256 | 5cf2468a731b… |
| 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: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
DAC_OVERRIDE | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
FSETID | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
FOWNER | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
MKNOD | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
NET_RAW | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
SETGID | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
SETUID | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
SETFCAP | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
SETPCAP | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
NET_BIND_SERVICE | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
SYS_CHROOT | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
KILL | removable | correct: :8080 proxied to caddy backend, haproxy uid=99 (non-root) |
AUDIT_WRITE | removable | correct: :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 | -cprintf <minimal-cfg> > /tmp/h.cfg && exec haproxy -f /tmp/h.cfg |
|---|---|
| security_opt | no-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
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/haproxy@sha256:e271912a69f3… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/haproxy.sh |
| workload sha256 | 5cf2468a731b… |
| 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: :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
- 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
# 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)"}