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
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/mysql@sha256:c592c15aaf4a… |
| validated date | 2026-07-17 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/mysql.sh |
| workload sha256 | 3aa5109ad4a4… |
| 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: query round-trip ok, mysqld uid=999 (non-root) |
DAC_OVERRIDE | required | container exited: find: '/var/lib/mysql-files': Permission denied |
FSETID | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
FOWNER | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
MKNOD | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
NET_RAW | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
SETGID | required | container exited: 2026-07-16T01:34:54.969592Z 0 [ERROR] [MY-010119] [Server] Aborting |
SETUID | required | container exited: 2026-07-16T01:34:55.536134Z 0 [ERROR] [MY-010119] [Server] Aborting |
SETFCAP | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
SETPCAP | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
NET_BIND_SERVICE | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
SYS_CHROOT | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
KILL | removable | correct: query round-trip ok, mysqld uid=999 (non-root) |
AUDIT_WRITE | removable | correct: 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_opt | no-new-privileges:true |
|---|---|
| env | MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_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
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/mysql@sha256:c592c15aaf4a… |
| validated date | 2026-07-17 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/mysql.sh |
| workload sha256 | 3aa5109ad4a4… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
/var/run/mysqld | required | container exited: 2026-07-16 20:55:19+00:00 [ERROR] [Entrypoint]: Unable to start server. |
/tmp | required | container 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.
| env | MYSQL_ROOT_PASSWORDMYSQL_DATABASEMYSQL_USERMYSQL_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
- 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
# 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."}