docker.io/library/nextcloud
validated pin current tags: 32
nextcloud — minimum Linux capabilities, derived by drop-test against the
default in-stack invocation (apache variant, MYSQL_* env against a mariadb
database, NEXTCLOUD_ADMIN_* auto-install). This profile trims the full
Docker default cap set 14 -> 6 — the same set as sibling wordpress, with
rsync-flavored evidence:
CHOWN + DAC_OVERRIDE + FOWNER — the entrypoint rsyncs ~700MB of sources
into the fresh /var/www/html VOLUME preserving www-data ownership,
permissions and times; any of the three dropped fails the copy
("rsync error: some files/attrs were not transferred (code 23)") and
the container exits.
SETGID + SETUID — the apache master/worker drop to www-data (SETUID
dropped = MPM fails AH02818; SETGID dropped = install never
completes, unixd alert).
NET_BIND_SERVICE — the :80 bind, ONLY under the pinned
net.ipv4.ip_unprivileged_port_start=1024 posture (docker defaults 0;
recorded structurally in run_config.sysctls — schema 1.4 — and in the
criteria doc).
Derived with the in-stack dependent-tier model (deps: the cataloged
mariadb:11.4). Trial lifecycle quirk encoded in the harness: a
fresh-docroot container cannot adopt an installed database — see
criteria/docker.io/library/nextcloud.md.
filesystem: read_only:true, tmpfs:[/var/run/apache2]. nextcloud (Apache+PHP)
rsyncs its source + stores config/data in /var/www/html, a declared VOLUME
(persistent, never tmpfs). Under a read-only rootfs the only rootfs write is
Apache's pid/lock dir /var/run/apache2; /var/log/apache2 and /tmp were
drop-tested and are NOT required. Derived by drop-test (in-stack mariadb dep).Use it
services:
nextcloud:
image: docker.io/library/nextcloud:32
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID]
security_opt: ["no-new-privileges:true"]
read_only: true
tmpfs:
- /var/run/apache2
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: CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/nextcloud@sha256:90fdc9f99fab… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/nextcloud.sh |
| workload sha256 | da8d4529fb8f… |
| 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 | required | container exited: rsync error: some files/attrs were not transferred (code 23) |
DAC_OVERRIDE | required | container exited: rsync error: some files/attrs were not transferred (code 23) |
FSETID | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
FOWNER | required | container exited: rsync error: some files/attrs were not transferred (code 23) |
MKNOD | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
NET_RAW | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
SETGID | required | install never completed after DB reset: [unixd:alert] setgid failure |
SETUID | required | container exited during install: [:emerg] AH02818: MPM run failed |
SETFCAP | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
SETPCAP | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
NET_BIND_SERVICE | required | container exited during install: AH00015: Unable to open logs |
SYS_CHROOT | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
KILL | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
AUDIT_WRITE | removable | correct: WebDAV PUT/GET round-trip ok, 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_opt | no-new-privileges:true |
|---|---|
| sysctls | net.ipv4.ip_unprivileged_port_start=1024 |
| env | MYSQL_HOSTMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORDNEXTCLOUD_ADMIN_USERNEXTCLOUD_ADMIN_PASSWORDNEXTCLOUD_TRUSTED_DOMAINS |
Workload script (profiles/workloads/nextcloud.sh)
#!/usr/bin/env bash
# Workload exerciser for the nextcloud reference image (apache variant,
# in-stack against a mariadb database per MYSQL_* env, admin auto-installed
# via NEXTCLOUD_ADMIN_*).
#
# Real function: wait for `occ status` to report installed, then a WebDAV
# PUT/GET round-trip as the admin — a real file write into nextcloud's data
# dir through the full apache -> php -> storage path — asserting the payload
# reads back byte-identical.
#
# Probe hygiene: occ runs as --user www-data (a root probe pollutes a caps
# minimum); WebDAV goes through a curl sidecar; responses are captured then
# matched (never `curl | grep -q` under pipefail — SIGPIPE false-negatives).
# Derivation-time only: a fresh-docroot container cannot adopt an installed
# database (it boots "installed: false"); the csd drop-test correctness check
# resets the dep database and restarts the target — see the criteria doc.
#
# Required env: NEXTCLOUDCONTAINER (target container name or id).
set -euo pipefail
: "${NEXTCLOUDCONTAINER:?NEXTCLOUDCONTAINER must be set}"
C="${NEXTCLOUDCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 180))
until docker exec --user www-data "$C" php occ status 2>/dev/null | grep -q "installed: true"; do
(( SECONDS >= deadline )) && { echo "nextcloud never reported installed (fresh-docroot + installed DB needs a DB reset — see criteria)" >&2; exit 1; }
sleep 5
done
sc -sf --max-time 15 -u csdadmin:CsdProbe-Pw-12345 -X PUT \
--data-binary "csd-payload-42" \
http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt >/dev/null
got="$(sc -s --max-time 15 -u csdadmin:CsdProbe-Pw-12345 \
http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt 2>/dev/null)"
if [ "$got" != "csd-payload-42" ]; then
echo "WebDAV round-trip mismatch (got: ${got:0:60})" >&2
exit 1
fi
Dimension: filesystem
read_only: true + tmpfs: /var/run/apache2
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/library/nextcloud@sha256:90fdc9f99fab… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/nextcloud.sh |
| workload sha256 | da8d4529fb8f… |
| 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/apache2 | required | container exited during install: AH00558: apache2: Could not reliably determine the server's fully qualified d |
/var/log/apache2 | removable | correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root) |
/tmp | removable | correct: WebDAV PUT/GET round-trip ok, 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.
| env | MYSQL_HOSTMYSQL_DATABASEMYSQL_USERMYSQL_PASSWORDNEXTCLOUD_ADMIN_USERNEXTCLOUD_ADMIN_PASSWORDNEXTCLOUD_TRUSTED_DOMAINS |
|---|
Workload script (profiles/workloads/nextcloud.sh)
#!/usr/bin/env bash
# Workload exerciser for the nextcloud reference image (apache variant,
# in-stack against a mariadb database per MYSQL_* env, admin auto-installed
# via NEXTCLOUD_ADMIN_*).
#
# Real function: wait for `occ status` to report installed, then a WebDAV
# PUT/GET round-trip as the admin — a real file write into nextcloud's data
# dir through the full apache -> php -> storage path — asserting the payload
# reads back byte-identical.
#
# Probe hygiene: occ runs as --user www-data (a root probe pollutes a caps
# minimum); WebDAV goes through a curl sidecar; responses are captured then
# matched (never `curl | grep -q` under pipefail — SIGPIPE false-negatives).
# Derivation-time only: a fresh-docroot container cannot adopt an installed
# database (it boots "installed: false"); the csd drop-test correctness check
# resets the dep database and restarts the target — see the criteria doc.
#
# Required env: NEXTCLOUDCONTAINER (target container name or id).
set -euo pipefail
: "${NEXTCLOUDCONTAINER:?NEXTCLOUDCONTAINER must be set}"
C="${NEXTCLOUDCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }
deadline=$((SECONDS + 180))
until docker exec --user www-data "$C" php occ status 2>/dev/null | grep -q "installed: true"; do
(( SECONDS >= deadline )) && { echo "nextcloud never reported installed (fresh-docroot + installed DB needs a DB reset — see criteria)" >&2; exit 1; }
sleep 5
done
sc -sf --max-time 15 -u csdadmin:CsdProbe-Pw-12345 -X PUT \
--data-binary "csd-payload-42" \
http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt >/dev/null
got="$(sc -s --max-time 15 -u csdadmin:CsdProbe-Pw-12345 \
http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt 2>/dev/null)"
if [ "$got" != "csd-payload-42" ]; then
echo "WebDAV round-trip mismatch (got: ${got:0:60})" >&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
# nextcloud — minimum Linux capabilities, derived by drop-test against the
# default in-stack invocation (apache variant, MYSQL_* env against a mariadb
# database, NEXTCLOUD_ADMIN_* auto-install). This profile trims the full
# Docker default cap set 14 -> 6 — the same set as sibling wordpress, with
# rsync-flavored evidence:
#
# CHOWN + DAC_OVERRIDE + FOWNER — the entrypoint rsyncs ~700MB of sources
# into the fresh /var/www/html VOLUME preserving www-data ownership,
# permissions and times; any of the three dropped fails the copy
# ("rsync error: some files/attrs were not transferred (code 23)") and
# the container exits.
# SETGID + SETUID — the apache master/worker drop to www-data (SETUID
# dropped = MPM fails AH02818; SETGID dropped = install never
# completes, unixd alert).
# NET_BIND_SERVICE — the :80 bind, ONLY under the pinned
# net.ipv4.ip_unprivileged_port_start=1024 posture (docker defaults 0;
# recorded structurally in run_config.sysctls — schema 1.4 — and in the
# criteria doc).
#
# Derived with the in-stack dependent-tier model (deps: the cataloged
# mariadb:11.4). Trial lifecycle quirk encoded in the harness: a
# fresh-docroot container cannot adopt an installed database — see
# criteria/docker.io/library/nextcloud.md.
# filesystem: read_only:true, tmpfs:[/var/run/apache2]. nextcloud (Apache+PHP)
# rsyncs its source + stores config/data in /var/www/html, a declared VOLUME
# (persistent, never tmpfs). Under a read-only rootfs the only rootfs write is
# Apache's pid/lock dir /var/run/apache2; /var/log/apache2 and /tmp were
# drop-tested and are NOT required. Derived by drop-test (in-stack mariadb dep).
#
schema_version: "1.5"
image: docker.io/library/nextcloud
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/nextcloud.html
applies_to:
tags: ["32"]
status: validated
dimensions:
capabilities:
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, 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/nextcloud@sha256:90fdc9f99fab6f8dcfb16f6010a2a615775f01ed8e9265fa10beed6b2f10cbae
validated_date: "2026-07-16"
duration_seconds: 900
confidence: high
workload: profiles/workloads/nextcloud.sh
workload_sha256: "da8d4529fb8f89576cd324840cac3f62ea71e3284bea62d722c68287f3623ba9"
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: [MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD,
NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_TRUSTED_DOMAINS]
drop_test:
checks:
- {removed: CHOWN, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
- {removed: DAC_OVERRIDE, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
- {removed: FSETID, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: FOWNER, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
- {removed: MKNOD, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: NET_RAW, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: SETGID, required: true, observed: "install never completed after DB reset: [unixd:alert] setgid failure"}
- {removed: SETUID, required: true, observed: "container exited during install: [:emerg] AH02818: MPM run failed"}
- {removed: SETFCAP, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: SETPCAP, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: NET_BIND_SERVICE, required: true, observed: "container exited during install: AH00015: Unable to open logs"}
- {removed: SYS_CHROOT, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: KILL, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: AUDIT_WRITE, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
filesystem:
read_only: true
tmpfs: [/var/run/apache2]
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/library/nextcloud@sha256:90fdc9f99fab6f8dcfb16f6010a2a615775f01ed8e9265fa10beed6b2f10cbae
validated_date: "2026-07-16"
duration_seconds: 77
confidence: high
workload: profiles/workloads/nextcloud.sh
workload_sha256: "da8d4529fb8f89576cd324840cac3f62ea71e3284bea62d722c68287f3623ba9"
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_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_TRUSTED_DOMAINS]
drop_test:
checks:
- {removed: /var/run/apache2, required: true, observed: "container exited during install: AH00558: apache2: Could not reliably determine the server's fully qualified d"}
- {removed: /var/log/apache2, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
- {removed: /tmp, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}