docker.io/library/mariadb
validated pin current tags: 11.4
mariadb — minimum Linux capabilities, derived by drop-test against mariadb's default (root-then-drop) invocation. This profile trims the full Docker default cap set 14 -> 2. capabilities: cap_drop:[ALL] + cap_add:[SETGID, SETUID]. The entrypoint starts as root and re-execs itself as the mysql user via gosu (SETUID/SETGID) — the one privilege step it always performs. Both are STARTUP caps, invisible to runtime observation. NOT required by default: CHOWN and DAC_OVERRIDE. The image ships /var/lib/mysql already owned by the mysql user, so a docker-managed volume (named or anonymous) inherits that ownership — the entrypoint's `find`-chown is a no-op, and because it re-execs as mysql early, initialisation writes are as the datadir's owner (no DAC_OVERRIDE). A FOREIGN-owned bind mount (a fresh host dir not owned by uid 999) is the exception: the find-chown then fires and CHOWN is required (DAC_OVERRIDE still is not). And a deployment that runs `user:` = the mysql uid against a pre-owned datadir skips the gosu drop entirely, shrinking the minimum to []. See criteria/docker.io/library/mariadb.md. filesystem: read_only:true, tmpfs:[/run/mysqld, /tmp]. The data dir /var/lib/mysql is a declared VOLUME (persistent). Under a read-only rootfs mariadbd needs /run/mysqld (unix socket + pid) and /tmp (temp files) writable. Derived by drop-test.
Use it
services:
mariadb:
image: docker.io/library/mariadb:11.4
cap_drop: [ALL]
cap_add: [SETGID, SETUID]
security_opt: ["no-new-privileges:true"]
read_only: true
tmpfs:
- /run/mysqld
- /tmp
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: SETGID, SETUID
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/mariadb@sha256:a794d9eb009e… |
| validated date | 2026-07-04 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/mariadb.sh |
| workload sha256 | db93f70a7282… |
| 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, mariadbd uid=999 (non-root) |
DAC_OVERRIDE | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
FSETID | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
FOWNER | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
MKNOD | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
NET_RAW | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
SETGID | required | container exited: error: failed switching to 'mysql': operation not permitted |
SETUID | required | container exited: error: failed switching to 'mysql': operation not permitted |
SETFCAP | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
SETPCAP | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
NET_BIND_SERVICE | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
SYS_CHROOT | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
KILL | removable | correct: query round-trip ok, mariadbd uid=999 (non-root) |
AUDIT_WRITE | removable | correct: query round-trip ok, mariadbd 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 | MARIADB_ROOT_PASSWORDMARIADB_DATABASEMARIADB_USERMARIADB_PASSWORD |
Workload script (profiles/workloads/mariadb.sh)
#!/usr/bin/env bash
# Workload exerciser for the mariadb reference image.
#
# Connects as the configured application user and drives a representative
# round-trip — CREATE a table, INSERT, SELECT it back, DROP. 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: MARIADBCONTAINER (target container name or id). The container is
# expected to have been started with MARIADB_USER / MARIADB_PASSWORD /
# MARIADB_DATABASE set (root@localhost is unix_socket-only in MariaDB 11.x).
set -euo pipefail
: "${MARIADBCONTAINER:?MARIADBCONTAINER must be set}"
C="${MARIADBCONTAINER}"
# Probes exec as the mysql user, never root — a root probe's own needs would
# pollute the derived minimum (the rabbitmq lesson).
deadline=$((SECONDS + 45))
until docker exec --user mysql "$C" sh -c 'MYSQL_PWD=$MARIADB_PASSWORD mariadb -u"$MARIADB_USER" -e "SELECT 1" >/dev/null 2>&1'; do
if (( SECONDS >= deadline )); then
echo "mariadb did not accept an app-user connection in 45s" >&2
exit 1
fi
sleep 1
done
out="$(docker exec --user mysql "$C" sh -c 'MYSQL_PWD=$MARIADB_PASSWORD mariadb -u"$MARIADB_USER" "$MARIADB_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: /run/mysqld, /tmp
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/mariadb@sha256:a794d9eb009e… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/mariadb.sh |
| workload sha256 | db93f70a7282… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
/run/mysqld | required | container exited: 2026-07-16 20:55:08+00:00 [ERROR] [Entrypoint]: Unable to start server. |
/tmp | required | container exited: |
Recorded invocation (run_config)
The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.
| env | MARIADB_ROOT_PASSWORDMARIADB_DATABASEMARIADB_USERMARIADB_PASSWORD |
|---|
Workload script (profiles/workloads/mariadb.sh)
#!/usr/bin/env bash
# Workload exerciser for the mariadb reference image.
#
# Connects as the configured application user and drives a representative
# round-trip — CREATE a table, INSERT, SELECT it back, DROP. 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: MARIADBCONTAINER (target container name or id). The container is
# expected to have been started with MARIADB_USER / MARIADB_PASSWORD /
# MARIADB_DATABASE set (root@localhost is unix_socket-only in MariaDB 11.x).
set -euo pipefail
: "${MARIADBCONTAINER:?MARIADBCONTAINER must be set}"
C="${MARIADBCONTAINER}"
# Probes exec as the mysql user, never root — a root probe's own needs would
# pollute the derived minimum (the rabbitmq lesson).
deadline=$((SECONDS + 45))
until docker exec --user mysql "$C" sh -c 'MYSQL_PWD=$MARIADB_PASSWORD mariadb -u"$MARIADB_USER" -e "SELECT 1" >/dev/null 2>&1'; do
if (( SECONDS >= deadline )); then
echo "mariadb did not accept an app-user connection in 45s" >&2
exit 1
fi
sleep 1
done
out="$(docker exec --user mysql "$C" sh -c 'MYSQL_PWD=$MARIADB_PASSWORD mariadb -u"$MARIADB_USER" "$MARIADB_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
# mariadb — minimum Linux capabilities, derived by drop-test against mariadb's
# default (root-then-drop) invocation. This profile trims the full Docker default
# cap set 14 -> 2.
#
# capabilities: cap_drop:[ALL] + cap_add:[SETGID, SETUID]. The entrypoint starts as
# root and re-execs itself as the mysql user via gosu (SETUID/SETGID) — the one
# privilege step it always performs. Both are STARTUP caps, invisible to runtime
# observation.
#
# NOT required by default: CHOWN and DAC_OVERRIDE. The image ships /var/lib/mysql
# already owned by the mysql user, so a docker-managed volume (named or anonymous)
# inherits that ownership — the entrypoint's `find`-chown is a no-op, and because it
# re-execs as mysql early, initialisation writes are as the datadir's owner (no
# DAC_OVERRIDE). A FOREIGN-owned bind mount (a fresh host dir not owned by uid 999)
# is the exception: the find-chown then fires and CHOWN is required (DAC_OVERRIDE
# still is not). And a deployment that runs `user:` = the mysql uid against a
# pre-owned datadir skips the gosu drop entirely, shrinking the minimum to [].
# See criteria/docker.io/library/mariadb.md.
# filesystem: read_only:true, tmpfs:[/run/mysqld, /tmp]. The data dir
# /var/lib/mysql is a declared VOLUME (persistent). Under a read-only rootfs
# mariadbd needs /run/mysqld (unix socket + pid) and /tmp (temp files) writable.
# Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/library/mariadb
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/mariadb.html
applies_to:
tags: ["11.4"]
status: validated
dimensions:
capabilities:
cap_drop: [ALL]
cap_add: [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/mariadb@sha256:a794d9eb009e20de605858a11f32f63b4075cbd197c650436f0e3b457e4caed7
validated_date: "2026-07-04"
duration_seconds: 80
confidence: high
workload: profiles/workloads/mariadb.sh
workload_sha256: "db93f70a728265fa0e9a798a17b4859ed408200a113782b3b7a284f5d88c65aa"
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: [MARIADB_ROOT_PASSWORD, MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD]
drop_test:
checks:
- {removed: CHOWN, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: DAC_OVERRIDE, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: FSETID, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: FOWNER, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: MKNOD, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: NET_RAW, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: SETGID, required: true, observed: "container exited: error: failed switching to 'mysql': operation not permitted"}
- {removed: SETUID, required: true, observed: "container exited: error: failed switching to 'mysql': operation not permitted"}
- {removed: SETFCAP, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: SETPCAP, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: NET_BIND_SERVICE, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: SYS_CHROOT, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: KILL, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
- {removed: AUDIT_WRITE, required: false, observed: "correct: query round-trip ok, mariadbd uid=999 (non-root)"}
filesystem:
read_only: true
tmpfs: [/run/mysqld, /tmp]
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/library/mariadb@sha256:a794d9eb009e20de605858a11f32f63b4075cbd197c650436f0e3b457e4caed7
validated_date: "2026-07-16"
duration_seconds: 43
confidence: high
workload: profiles/workloads/mariadb.sh
workload_sha256: "db93f70a728265fa0e9a798a17b4859ed408200a113782b3b7a284f5d88c65aa"
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: [MARIADB_ROOT_PASSWORD, MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD]
drop_test:
checks:
- {removed: /run/mysqld, required: true, observed: "container exited: 2026-07-16 20:55:08+00:00 [ERROR] [Entrypoint]: Unable to start server."}
- {removed: /tmp, required: true, observed: "container exited: "}