catalog / docker.io/library/httpd

docker.io/library/httpd

validated  pin current  tags: 2.4

httpd (Apache) — minimum Linux capabilities, derived by drop-test against the
default invocation. This profile trims the full Docker default cap set 14 -> 3.

capabilities: cap_drop:[ALL] + cap_add:[NET_BIND_SERVICE, SETGID, SETUID].
The MASTER/WORKER split shape (the caddy contrast): the master stays root and
forks workers that setuid/setgid to www-data — so both the :80 bind and the
worker drop are load-bearing. NET_BIND_SERVICE is scoped to the hardened
sysctl posture net.ipv4.ip_unprivileged_port_start=1024 pinned during
derivation (docker defaults 0, where it reads falsely-removable — same scope
note as traefik).

THE SHARP EDGE THIS PROFILE PROVES: with SETGID dropped, httpd KEEPS SERVING
with ROOT workers ("workers running as ROOT: uid=0" in the evidence) — a
content-only check false-passes and would derive the drop as safe while
silently removing the privilege separation. The workload's non-root worker
uid assert is mandatory for any re-derivation.
See criteria/docker.io/library/httpd.md.
filesystem: read_only:true, tmpfs:[/usr/local/apache2/logs]. httpd writes its
pid + logs there at startup and fails to start under a read-only rootfs without
it (AH00099: could not create httpd.pid); /tmp was drop-tested and is NOT
required. Derived by drop-test.

Use it

services:
  httpd:
    image: docker.io/library/httpd:2.4
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE, SETGID, SETUID]
    security_opt: ["no-new-privileges:true"]
    read_only: true
    tmpfs:
      - /usr/local/apache2/logs

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, SETGID, SETUID

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

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: default page served, worker uid=33 (non-root)
DAC_OVERRIDEremovablecorrect: default page served, worker uid=33 (non-root)
FSETIDremovablecorrect: default page served, worker uid=33 (non-root)
FOWNERremovablecorrect: default page served, worker uid=33 (non-root)
MKNODremovablecorrect: default page served, worker uid=33 (non-root)
NET_RAWremovablecorrect: default page served, worker uid=33 (non-root)
SETGIDrequiredhttpd workers running as ROOT or not found (drop failed): uid=0
SETUIDrequiredcontainer exited: [core:alert] AH00050: Child returned a Fatal error... Apache is exiting!
SETFCAPremovablecorrect: default page served, worker uid=33 (non-root)
SETPCAPremovablecorrect: default page served, worker uid=33 (non-root)
NET_BIND_SERVICErequiredcontainer failed to start: AH00015: Unable to open logs
SYS_CHROOTremovablecorrect: default page served, worker uid=33 (non-root)
KILLremovablecorrect: default page served, worker uid=33 (non-root)
AUDIT_WRITEremovablecorrect: default page served, worker uid=33 (non-root)

Recorded invocation (run_config)

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

security_optno-new-privileges:true
sysctlsnet.ipv4.ip_unprivileged_port_start=1024
Workload script (profiles/workloads/httpd.sh)
#!/usr/bin/env bash
# Workload exerciser for the httpd (Apache) reference image.
#
# GET / must return the default page body, fetched from a curl sidecar sharing
# the target's network namespace (the image ships no curl/wget). The
# master/worker uid assert lives in the drop-test correctness check (see the
# criteria doc) and is MANDATORY there: with SETUID/SETGID dropped, httpd
# keeps serving with ROOT workers — the content check alone false-passes.
#
# Required env: HTTPDCONTAINER (target container name or id).
set -euo pipefail

: "${HTTPDCONTAINER:?HTTPDCONTAINER must be set}"
C="${HTTPDCONTAINER}"
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:80/ 2>/dev/null)"
    case "$body" in *"It works!"*) break;; esac
    (( SECONDS >= deadline )) && { echo "default page never served" >&2; exit 1; }
    sleep 1
done

Dimension: filesystem

read_only: true + tmpfs: /usr/local/apache2/logs

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

Drop-test evidence

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

RemovedVerdictObserved
/usr/local/apache2/logsrequiredcontainer failed to start: [Thu Jul 16 17:32:57.795602 2026] [core:error] [pid 1:tid 1] AH00100: httpd: could not log pid to file /usr/local/apache2/logs/httpd.pid
/tmpremovablecorrect: default page served, worker uid=33 (non-root)
Workload script (profiles/workloads/httpd.sh)
#!/usr/bin/env bash
# Workload exerciser for the httpd (Apache) reference image.
#
# GET / must return the default page body, fetched from a curl sidecar sharing
# the target's network namespace (the image ships no curl/wget). The
# master/worker uid assert lives in the drop-test correctness check (see the
# criteria doc) and is MANDATORY there: with SETUID/SETGID dropped, httpd
# keeps serving with ROOT workers — the content check alone false-passes.
#
# Required env: HTTPDCONTAINER (target container name or id).
set -euo pipefail

: "${HTTPDCONTAINER:?HTTPDCONTAINER must be set}"
C="${HTTPDCONTAINER}"
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:80/ 2>/dev/null)"
    case "$body" in *"It works!"*) break;; esac
    (( SECONDS >= deadline )) && { echo "default page never served" >&2; exit 1; }
    sleep 1
done

Evidence & provenance

Raw profile YAML
# httpd (Apache) — minimum Linux capabilities, derived by drop-test against the
# default invocation. This profile trims the full Docker default cap set 14 -> 3.
#
# capabilities: cap_drop:[ALL] + cap_add:[NET_BIND_SERVICE, SETGID, SETUID].
# The MASTER/WORKER split shape (the caddy contrast): the master stays root and
# forks workers that setuid/setgid to www-data — so both the :80 bind and the
# worker drop are load-bearing. NET_BIND_SERVICE is scoped to the hardened
# sysctl posture net.ipv4.ip_unprivileged_port_start=1024 pinned during
# derivation (docker defaults 0, where it reads falsely-removable — same scope
# note as traefik).
#
# THE SHARP EDGE THIS PROFILE PROVES: with SETGID dropped, httpd KEEPS SERVING
# with ROOT workers ("workers running as ROOT: uid=0" in the evidence) — a
# content-only check false-passes and would derive the drop as safe while
# silently removing the privilege separation. The workload's non-root worker
# uid assert is mandatory for any re-derivation.
# See criteria/docker.io/library/httpd.md.
# filesystem: read_only:true, tmpfs:[/usr/local/apache2/logs]. httpd writes its
# pid + logs there at startup and fails to start under a read-only rootfs without
# it (AH00099: could not create httpd.pid); /tmp was drop-tested and is NOT
# required. Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/library/httpd
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/httpd.html
applies_to:
  tags: ["2.4"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE, SETGID, SETUID]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/httpd@sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
      validated_date: "2026-07-16"
      duration_seconds: 240
      confidence: high
      workload: profiles/workloads/httpd.sh
      workload_sha256: "94d1ea3488804864d96ef091f91cb619b2bc01975952b504549071c56d55aa42"
      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"]
        sysctls: ["net.ipv4.ip_unprivileged_port_start=1024"]
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: DAC_OVERRIDE, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: FSETID, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: FOWNER, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: MKNOD, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: NET_RAW, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: SETGID, required: true, observed: "httpd workers running as ROOT or not found (drop failed): uid=0"}
          - {removed: SETUID, required: true, observed: "container exited: [core:alert] AH00050: Child returned a Fatal error... Apache is exiting!"}
          - {removed: SETFCAP, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: SETPCAP, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: NET_BIND_SERVICE, required: true, observed: "container failed to start: AH00015: Unable to open logs"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: KILL, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}
  filesystem:
    read_only: true
    tmpfs: [/usr/local/apache2/logs]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/httpd@sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
      validated_date: "2026-07-16"
      duration_seconds: 5
      confidence: high
      workload: profiles/workloads/httpd.sh
      workload_sha256: "94d1ea3488804864d96ef091f91cb619b2bc01975952b504549071c56d55aa42"
      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: /usr/local/apache2/logs, required: true, observed: "container failed to start: [Thu Jul 16 17:32:57.795602 2026] [core:error] [pid 1:tid 1] AH00100: httpd: could not log pid to file /usr/local/apache2/logs/httpd.pid"}
          - {removed: /tmp, required: false, observed: "correct: default page served, worker uid=33 (non-root)"}