catalog / docker.io/library/mysql

docker.io/library/mysql

validated  pin current  tags: 8.4

mysql — minimum Linux capabilities, derived by drop-test against mysql's
default (root-then-drop) invocation. This profile trims the full Docker default
cap set 14 -> 3.

capabilities: cap_drop:[ALL] + cap_add:[DAC_OVERRIDE, SETGID, SETUID]. The
entrypoint starts as root, walks the data dirs with `find`, and re-execs itself
as the mysql user via gosu (SETGID/SETUID). DAC_OVERRIDE is the divergence from
sibling mariadb: the image ships /var/lib/mysql-files as mysql-owned mode 750,
and the entrypoint's ROOT-phase `find` must traverse it — root without
DAC_OVERRIDE is subject to normal DAC checks, so the container exits with
"find: '/var/lib/mysql-files': Permission denied". All three are STARTUP caps,
invisible to runtime observation.

NOT required by default: CHOWN. The image ships /var/lib/mysql (and
/var/run/mysqld) already owned by the mysql user mode 1777, so a docker-managed
volume inherits that ownership and the entrypoint's `find`-chown is a no-op. A
FOREIGN-owned bind mount (a fresh host dir not owned by uid 999) re-introduces
CHOWN. A deployment that runs `user:` = the mysql uid against a pre-owned
datadir skips the drop and the root-phase find entirely, shrinking the minimum
to []. See criteria/docker.io/library/mysql.md.
filesystem: read_only:true, tmpfs:[/tmp, /var/run/mysqld]. The data dir
/var/lib/mysql is a declared VOLUME (persistent). Under a read-only rootfs
mysqld needs /var/run/mysqld (unix socket + pid) and /tmp (temp files)
writable. Derived by drop-test.

Use it

services:
  mysql:
    image: docker.io/library/mysql:8.4
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, SETGID, SETUID]
    security_opt: ["no-new-privileges:true"]
    read_only: true
    tmpfs:
      - /tmp
      - /var/run/mysqld

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

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

Drop-test evidence

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

RemovedVerdictObserved
CHOWNremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
DAC_OVERRIDErequiredcontainer exited: find: '/var/lib/mysql-files': Permission denied
FSETIDremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
FOWNERremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
MKNODremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
NET_RAWremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
SETGIDrequiredcontainer exited: 2026-07-16T01:34:54.969592Z 0 [ERROR] [MY-010119] [Server] Aborting
SETUIDrequiredcontainer exited: 2026-07-16T01:34:55.536134Z 0 [ERROR] [MY-010119] [Server] Aborting
SETFCAPremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
SETPCAPremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
NET_BIND_SERVICEremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
SYS_CHROOTremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
KILLremovablecorrect: query round-trip ok, mysqld uid=999 (non-root)
AUDIT_WRITEremovablecorrect: query round-trip ok, mysqld uid=999 (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
envMYSQL_ROOT_PASSWORD
MYSQL_DATABASE
MYSQL_USER
MYSQL_PASSWORD
Workload script (profiles/workloads/mysql.sh)
#!/usr/bin/env bash
# Workload exerciser for the mysql reference image.
#
# Connects as the configured application user and drives a representative
# round-trip — CREATE a table, INSERT, SELECT it back, DROP — mirroring the
# official docker-library mysql-basics test shape. Returns 0 if every step
# succeeded. The non-root privilege-drop (the entrypoint's gosu root->mysql)
# is asserted by the drop-test correctness check (see the criteria doc), not
# here.
#
# Required env: MYSQLCONTAINER (target container name or id). The container is
# expected to have been started with MYSQL_USER / MYSQL_PASSWORD /
# MYSQL_DATABASE (and MYSQL_ROOT_PASSWORD, which the image requires) set.
set -euo pipefail

: "${MYSQLCONTAINER:?MYSQLCONTAINER must be set}"
C="${MYSQLCONTAINER}"

# mysql 8.x first-boot initialisation is slower than mariadb; allow 90s. The
# app-user connect proves the real server is up and init created the user —
# not the transient bootstrap server.
deadline=$((SECONDS + 90))
until docker exec "$C" sh -c 'MYSQL_PWD=$MYSQL_PASSWORD mysql -u"$MYSQL_USER" -e "SELECT 1" >/dev/null 2>&1'; do
    if (( SECONDS >= deadline )); then
        echo "mysql did not accept an app-user connection in 90s" >&2
        exit 1
    fi
    sleep 1
done

out="$(docker exec "$C" sh -c 'MYSQL_PWD=$MYSQL_PASSWORD mysql -u"$MYSQL_USER" "$MYSQL_DATABASE" -N -e "CREATE TABLE IF NOT EXISTS csd_probe(id INT); INSERT INTO csd_probe VALUES(42); SELECT id FROM csd_probe LIMIT 1; DROP TABLE csd_probe;"' 2>&1)"
if [ "$out" != 42 ]; then
    echo "workload round-trip failed (got: ${out##*$'\n'})" >&2
    exit 1
fi

Dimension: filesystem

read_only: true + tmpfs: /tmp, /var/run/mysqld

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

Drop-test evidence

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

RemovedVerdictObserved
/var/run/mysqldrequiredcontainer exited: 2026-07-16 20:55:19+00:00 [ERROR] [Entrypoint]: Unable to start server.
/tmprequiredcontainer exited: 2026-07-16T20:55:20.701873Z 0 [System] [MY-015018] [Server] MySQL Server Initialization - end.

Recorded invocation (run_config)

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

envMYSQL_ROOT_PASSWORD
MYSQL_DATABASE
MYSQL_USER
MYSQL_PASSWORD
Workload script (profiles/workloads/mysql.sh)
#!/usr/bin/env bash
# Workload exerciser for the mysql reference image.
#
# Connects as the configured application user and drives a representative
# round-trip — CREATE a table, INSERT, SELECT it back, DROP — mirroring the
# official docker-library mysql-basics test shape. Returns 0 if every step
# succeeded. The non-root privilege-drop (the entrypoint's gosu root->mysql)
# is asserted by the drop-test correctness check (see the criteria doc), not
# here.
#
# Required env: MYSQLCONTAINER (target container name or id). The container is
# expected to have been started with MYSQL_USER / MYSQL_PASSWORD /
# MYSQL_DATABASE (and MYSQL_ROOT_PASSWORD, which the image requires) set.
set -euo pipefail

: "${MYSQLCONTAINER:?MYSQLCONTAINER must be set}"
C="${MYSQLCONTAINER}"

# mysql 8.x first-boot initialisation is slower than mariadb; allow 90s. The
# app-user connect proves the real server is up and init created the user —
# not the transient bootstrap server.
deadline=$((SECONDS + 90))
until docker exec "$C" sh -c 'MYSQL_PWD=$MYSQL_PASSWORD mysql -u"$MYSQL_USER" -e "SELECT 1" >/dev/null 2>&1'; do
    if (( SECONDS >= deadline )); then
        echo "mysql did not accept an app-user connection in 90s" >&2
        exit 1
    fi
    sleep 1
done

out="$(docker exec "$C" sh -c 'MYSQL_PWD=$MYSQL_PASSWORD mysql -u"$MYSQL_USER" "$MYSQL_DATABASE" -N -e "CREATE TABLE IF NOT EXISTS csd_probe(id INT); INSERT INTO csd_probe VALUES(42); SELECT id FROM csd_probe LIMIT 1; DROP TABLE csd_probe;"' 2>&1)"
if [ "$out" != 42 ]; then
    echo "workload round-trip failed (got: ${out##*$'\n'})" >&2
    exit 1
fi

Evidence & provenance

Raw profile YAML
# mysql — minimum Linux capabilities, derived by drop-test against mysql's
# default (root-then-drop) invocation. This profile trims the full Docker default
# cap set 14 -> 3.
#
# capabilities: cap_drop:[ALL] + cap_add:[DAC_OVERRIDE, SETGID, SETUID]. The
# entrypoint starts as root, walks the data dirs with `find`, and re-execs itself
# as the mysql user via gosu (SETGID/SETUID). DAC_OVERRIDE is the divergence from
# sibling mariadb: the image ships /var/lib/mysql-files as mysql-owned mode 750,
# and the entrypoint's ROOT-phase `find` must traverse it — root without
# DAC_OVERRIDE is subject to normal DAC checks, so the container exits with
# "find: '/var/lib/mysql-files': Permission denied". All three are STARTUP caps,
# invisible to runtime observation.
#
# NOT required by default: CHOWN. The image ships /var/lib/mysql (and
# /var/run/mysqld) already owned by the mysql user mode 1777, so a docker-managed
# volume inherits that ownership and the entrypoint's `find`-chown is a no-op. A
# FOREIGN-owned bind mount (a fresh host dir not owned by uid 999) re-introduces
# CHOWN. A deployment that runs `user:` = the mysql uid against a pre-owned
# datadir skips the drop and the root-phase find entirely, shrinking the minimum
# to []. See criteria/docker.io/library/mysql.md.
# filesystem: read_only:true, tmpfs:[/tmp, /var/run/mysqld]. The data dir
# /var/lib/mysql is a declared VOLUME (persistent). Under a read-only rootfs
# mysqld needs /var/run/mysqld (unix socket + pid) and /tmp (temp files)
# writable. Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/library/mysql
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/mysql.html
applies_to:
  tags: ["8.4"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, 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/mysql@sha256:c592c15aaf4a1961e15d82eb31ea5987dda862d1c4b1e93424438c0e91dc1f8d
      validated_date: "2026-07-17"
      duration_seconds: 540
      confidence: high
      workload: profiles/workloads/mysql.sh
      workload_sha256: "3aa5109ad4a42b7a5ba0458b77a9d03766007cf17926ebfd565bc112ca391566"
      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"]
        mounts: []
        env: [MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD]
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: DAC_OVERRIDE, required: true, observed: "container exited: find: '/var/lib/mysql-files': Permission denied"}
          - {removed: FSETID, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: FOWNER, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: MKNOD, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: NET_RAW, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: SETGID, required: true, observed: "container exited: 2026-07-16T01:34:54.969592Z 0 [ERROR] [MY-010119] [Server] Aborting"}
          - {removed: SETUID, required: true, observed: "container exited: 2026-07-16T01:34:55.536134Z 0 [ERROR] [MY-010119] [Server] Aborting"}
          - {removed: SETFCAP, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: SETPCAP, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: NET_BIND_SERVICE, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: KILL, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: query round-trip ok, mysqld uid=999 (non-root)"}
  filesystem:
    read_only: true
    tmpfs: [/tmp, /var/run/mysqld]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/mysql@sha256:c592c15aaf4a1961e15d82eb31ea5987dda862d1c4b1e93424438c0e91dc1f8d
      validated_date: "2026-07-17"
      duration_seconds: 12
      confidence: high
      workload: profiles/workloads/mysql.sh
      workload_sha256: "3aa5109ad4a42b7a5ba0458b77a9d03766007cf17926ebfd565bc112ca391566"
      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: [MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD]
      drop_test:
        checks:
          - {removed: /var/run/mysqld, required: true, observed: "container exited: 2026-07-16 20:55:19+00:00 [ERROR] [Entrypoint]: Unable to start server."}
          - {removed: /tmp, required: true, observed: "container exited: 2026-07-16T20:55:20.701873Z 0 [System] [MY-015018] [Server] MySQL Server Initialization - end."}