From 5d0cd8de090349556d9d6843963e01b59afd76e3 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Mon, 24 Aug 2026 13:07:52 +0000 Subject: [PATCH 01/12] Run ownCloud 11 on its supported container ownCloud 11 no longer supports the manual PHP deployment used by the v18 appliance, and Trixie's PHP 8.4 exposes runtime incompatibilities in that path. Use the official amd64 image pinned by manifest digest while retaining Debian Apache, MariaDB, Redis, Adminer, Docker, and Skopeo around it. Expose the application only through Apache HTTPS, isolate database and cache traffic on a dedicated bridge, rotate backend credentials before first startup, and provide supervised digest-verified image updates. Add focused acceptance for administrator authentication, WebDAV file CRUD, occ user management, database persistence, Redis authentication, Adminer, package sources, and updater no-op behavior. Verified with disposable Trixie manual-install and bridge-gateway probes, shell and Python syntax checks, and git diff --check. Exact FAB/runtime acceptance follows from this clean commit. --- Makefile | 2 - README.rst | 20 +- changelog | 12 +- conf.d/downloads | 25 ++- conf.d/main | 85 ++++----- docs/v19.0-testing.md | 99 ++++++++++ .../etc/apache2/sites-available/owncloud.conf | 11 +- overlay/etc/cron.d/owncloud | 2 +- .../mariadb.service.d/owncloud-network.conf | 3 + .../systemd/system/owncloud-network.service | 13 ++ overlay/etc/systemd/system/owncloud.service | 15 ++ .../owncloud-network.conf | 3 + overlay/usr/lib/inithooks/bin/owncloud.py | 55 ++++-- .../firstboot.d/20regen-owncloud-secrets | 27 +-- overlay/usr/local/bin/turnkey-occ | 15 +- overlay/usr/local/lib/owncloud/network-ready | 17 ++ overlay/usr/local/lib/owncloud/run | 18 ++ overlay/usr/local/lib/owncloud/wait-firstboot | 14 ++ overlay/usr/local/sbin/owncloud-update | 58 ++++++ plan/main | 25 +-- tests/v19.sh | 179 ++++++++++++++++++ 21 files changed, 569 insertions(+), 129 deletions(-) create mode 100644 docs/v19.0-testing.md create mode 100644 overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf create mode 100644 overlay/etc/systemd/system/owncloud-network.service create mode 100644 overlay/etc/systemd/system/owncloud.service create mode 100644 overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf create mode 100755 overlay/usr/local/lib/owncloud/network-ready create mode 100755 overlay/usr/local/lib/owncloud/run create mode 100755 overlay/usr/local/lib/owncloud/wait-firstboot create mode 100755 overlay/usr/local/sbin/owncloud-update create mode 100755 tests/v19.sh diff --git a/Makefile b/Makefile index 3ddea93..567dfd2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,2 @@ -PHP_VERSION=7.4 - include $(FAB_PATH)/common/mk/turnkey/lamp.mk include $(FAB_PATH)/common/mk/turnkey.mk diff --git a/README.rst b/README.rst index 7a72c83..44ffd94 100644 --- a/README.rst +++ b/README.rst @@ -9,16 +9,22 @@ wherever you are, when you need it. This appliance includes all the standard features in `TurnKey Core`_, and on top of that: -- ownCloud Server: +- ownCloud Server 11: - - Installed from official zip file to /var/www/owncloud. - - Data directory (/var/www/owncloud-data) outside the webroot (security). + - Runs from the official ownCloud container image, pinned to a verified + amd64 manifest digest. + - Stores persistent configuration and user files under + /var/lib/owncloud. + - Uses Debian MariaDB and Redis services on a dedicated local Docker + network. - Includes occ_ script for command line administration and configuration. Also includes turnkey-occ_ wrapper script (runs occ as www-data user). - **Security note**: Updates to ownCloud may require supervision so - they **ARE NOT** configured to install automatically. See `ownCloud - documentation`_ for upgrading. + **Security note**: ownCloud updates require supervision and are not + installed automatically. Use ``owncloud-update --check VERSION`` to + inspect the official amd64 image digest, then run + ``owncloud-update VERSION DIGEST`` after reviewing the release and + backing up the appliance. See `ownCloud documentation`_ for upgrading. - SSL support out of the box. - `Adminer`_ administration frontend for MySQL (listening on port @@ -39,5 +45,5 @@ Credentials *(passwords set at first boot)* .. _TurnKey Core: https://www.turnkeylinux.org/core .. _occ: https://doc.owncloud.com/server/admin_manual/configuration/server/occ_command.html .. _turnkey-occ: https://github.com/turnkeylinux-apps/owncloud/blob/master/overlay/usr/local/bin/turnkey-occ -.. _ownCloud documentation: https://doc.owncloud.org/server/admin_manual/maintenance/upgrade.html +.. _ownCloud documentation: https://doc.owncloud.com/server/11.0/admin_manual/maintenance/upgrading/manual_upgrade.html .. _Adminer: https://www.adminer.org diff --git a/changelog b/changelog index 9630130..a7b463b 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,14 @@ +turnkey-owncloud-19.0 (1) turnkey; urgency=low + + * Run ownCloud Server 11.0.0 from the official container image pinned to + its amd64 manifest digest. + + * Use Debian 13 Trixie for Docker, Apache, MariaDB, Redis and Adminer. + + * Add a supervised ownCloud container update command. + + -- TurnKey Linux Developers Mon, 24 Aug 2026 13:00:00 +0000 + turnkey-owncloud-18.0 (1) turnkey; urgency=low * Includes latest stable ownCloud - v10.13.1. @@ -293,4 +304,3 @@ turnkey-owncloud-12.0 (1) turnkey; urgency=low appliances. Here we only describe changes specific to this appliance. -- Alon Swartz Wed, 01 Aug 2012 08:00:00 +0200 - diff --git a/conf.d/downloads b/conf.d/downloads index f2a5c1a..f80e5b3 100755 --- a/conf.d/downloads +++ b/conf.d/downloads @@ -1,10 +1,21 @@ -#!/bin/bash -ex +#!/bin/bash +set -Eeuo pipefail -dl() { - [ "$FAB_HTTP_PROXY" ] && PROXY="--proxy $FAB_HTTP_PROXY" - cd $2; curl -L -f $PROXY $1 > $3; cd - -} +image=owncloud/server@sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366 +archive=/usr/local/src/owncloud-server.tar +local_tag=owncloud/server:11.0.0-turnkey-dbebc24fe77a -URL="https://download.owncloud.com/server/stable/owncloud-complete-20230906.zip" +if [[ -n ${FAB_HTTP_PROXY:-} ]]; then + export HTTP_PROXY=$FAB_HTTP_PROXY + export HTTPS_PROXY=$FAB_HTTP_PROXY +fi -dl $URL /usr/local/src owncloud.zip +skopeo copy --override-os linux --override-arch amd64 \ + "docker://docker.io/$image" "docker-archive:$archive:$local_tag" + +cat > /usr/local/src/owncloud-image-source <ownCloud Appliance \– Powered by TurnKey Linux|" $TEMPLATE +REDIS_PASS=$(mcookie) # configure apache a2dissite 000-default a2ensite owncloud.conf a2enmod rewrite a2enmod headers -a2enmod env -a2enmod dir -a2enmod mime +a2enmod proxy +a2enmod proxy_http # start services service mysql start @@ -36,40 +19,48 @@ service apache2 start # setup the database mysqladmin create $DB_NAME -mysql --batch --execute "grant all privileges on $DB_NAME.* to $DB_USER@localhost identified by '$DB_PASS'; flush privileges;" +mysql --batch --execute "CREATE USER '$DB_USER'@'172.28.0.%' IDENTIFIED BY '$DB_PASS'; GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'172.28.0.%'; FLUSH PRIVILEGES;" -# command-line install -chown -R www-data:www-data $WEBROOT -cd $WEBROOT -php occ maintenance:install --database "mysql" \ - --database-name "$DB_NAME" --database-user "$DB_USER" --database-pass "$DB_PASS" \ - --admin-user "$ADMIN_NAME" --admin-pass "$ADMIN_PASS" \ - --data-dir "$DATAROOT" +install -d -m 700 /etc/owncloud +install -d -m 700 /var/lib/owncloud +install -m 600 /usr/local/src/owncloud-image-source /etc/owncloud/image-source -# enable notify owncloud that we are using cron -mysql --batch --database=$DB_NAME --execute "INSERT INTO oc_appconfig (appid, configkey, configvalue) VALUES ('core', 'backgroundjobs_mode', 'cron');" - -# adjust owncloud config for redis -sed -i '/^);/d' $CONF -cat >> $CONF < '\OC\Memcache\Redis', - 'redis' => [ - 'host' => '/var/run/redis/redis.sock', - 'port' => 0, - ], - 'memcache.locking' => '\OC\Memcache\Redis', -); +cat > /etc/owncloud/image.conf < /etc/owncloud/owncloud.env < /etc/mysql/mariadb.conf.d/60-owncloud.cnf <<'EOF' +[mariadbd] +bind-address = 172.28.0.1 +EOF -chown -R www-data:www-data $WEBROOT $DATAROOT +sed -i 's/^bind .*/bind 172.28.0.1/' /etc/redis/redis.conf +sed -i "s/^# requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf # stop services service mysql stop service apache2 stop + +systemctl enable docker.service owncloud-network.service owncloud.service diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..18d4f37 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,99 @@ +# ownCloud 19.0 testing + +## Scope and source decision + +ownCloud Server 11 supports PHP 8.3 or newer but supports deployment only +through the official ownCloud container image. Debian 13 Trixie provides PHP +8.4, so continuing the v18 manual archive installation would leave the +appliance outside ownCloud's maintained deployment contract. A disposable +Trixie probe confirmed that the 11.0.0 manual archive installs but fails when +it next opens its SQLite database on PHP 8.4. + +The appliance therefore uses the official ownCloud Server 11.0.0 image for +amd64, pinned to manifest digest +`sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366`. +Skopeo retrieves and verifies that digest during the build. Docker, Skopeo, +Apache, MariaDB, Redis and Adminer come from signed Debian Trixie repositories. + +The ownCloud container joins a dedicated `172.28.0.0/24` bridge. Debian +MariaDB and Redis listen only on its gateway, `172.28.0.1`, and Redis requires +a generated password. The ownCloud HTTP port binds only to host loopback. +Apache provides the public HTTPS endpoint. Persistent ownCloud configuration +and user data are stored under `/var/lib/owncloud`. + +## Acceptance command + +```sh +TKLDEV_CONTAINER=tkldev19-wave2 \ +TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ +TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +/sandboxed-git/turnkey/tools/test-v19-appliance owncloud \ + --source /home/agent/.local/worktrees/turnkey-apps/owncloud/wish-owncloud-v19-trixie +``` + +The command builds the Trixie root filesystem, imports it into Docker, boots +the normal systemd and firstboot path, then runs `tests/v19.sh`. The accepted +run, tested source, harness commit, report path and exact installed versions +are recorded below after exact acceptance passes. + +## README crosswalk + +| README contract | Focused check | Required result | Evidence | +| --- | --- | --- | --- | +| ownCloud is available through the HTTPS web endpoint | Request `/` and `/status.php` through Apache | The login page and installed ownCloud 11 status are returned over HTTPS; HTTP redirects to HTTPS | `tests/v19.sh` | +| The firstboot `admin` credential controls ownCloud | Request the authenticated OCS user endpoint | The response identifies the `admin` account | `tests/v19.sh` | +| Users can store and retrieve files | Create, read, update and delete a file through WebDAV, with MariaDB readback | Each content comparison passes, the database records the file and the deleted path returns HTTP 404 | `tests/v19.sh` | +| `turnkey-occ` provides command-line administration as `www-data` | Create, list and delete a temporary user through the wrapper | The user appears in both `occ` and MariaDB before deletion | `tests/v19.sh` | +| Persistent data is outside the application image | Inspect the running container and its `/mnt/data` mount | The official image runs with `/var/lib/owncloud` mounted at `/mnt/data` | `tests/v19.sh` and harness runtime evidence | +| MariaDB and Redis are private application backends | Inspect listeners, Docker network addressing and Redis authentication | Both services listen only on the private bridge gateway; Redis rejects unauthenticated requests | `tests/v19.sh` | +| Adminer manages MariaDB over HTTPS on port 12322 | Submit the firstboot `adminer` login to the ownCloud database | Adminer accepts the credentials and exposes the ownCloud database | `tests/v19.sh` | +| ownCloud and Debian components have maintained update channels | Query `owncloud-update --check` and refresh APT metadata without installing changes | The official image digest and eligible signed Trixie candidates are returned; installed versions and the running image remain unchanged | `tests/v19.sh` | +| Webmin, SSH, Postfix, backup and other base services come from the inherited appliance layers | Exercise app-specific services and cite the unchanged Core and LAMP boundaries | Application services pass; inherited services retain their existing contracts | Core 19 and LAMP 19 acceptance reports | + +## Update channel + +`owncloud-update --check VERSION` resolves the current official amd64 digest +for an explicit ownCloud version tag without changing the appliance. After an +administrator reviews the release and takes a backup, +`owncloud-update VERSION sha256:DIGEST` requires the reviewed digest to match +the official tag, pulls that exact image, assigns a digest-derived local tag, +updates `/etc/owncloud/image.conf` and restarts the service. ownCloud performs +its supported database migration when the new container starts. + +Debian and TurnKey components continue through their signed Trixie APT +repositories. The focused updater check refreshes metadata, verifies eligible +candidates and proves that the check itself leaves installed packages and the +running ownCloud image unchanged. + +## Probe evidence + +The official 11.0.0 image was exercised in a disposable bridge setup before +implementation. It connected to Trixie MariaDB and Redis on the bridge +gateway, initialized the administrator, reported version 11.0.0, created a +second user and persisted both users in MariaDB. The failed manual archive and +Unix-socket connection probes are recorded as disproved assumptions rather +than supported deployment paths. + +## Accepted evidence + +Pending the exact acceptance command. + +## Known limitations + +The Docker acceptance path does not exercise the installer, kernel, +bootloader or physical hardware. It does exercise the appliance's nested +Docker daemon in the privileged systemd runtime, the same services and +network layout used on a normal installed appliance. + +ownCloud Server 11 supports only container deployment. Administrators who add +applications requiring extra operating-system binaries must derive and pin a +custom image as described by ownCloud. This appliance intentionally keeps the +official image unchanged. + +## Deferred issues + +The acceptance flow covers the administrator endpoint, WebDAV file lifecycle, +MariaDB persistence, Redis authentication and command-line user management. +Desktop and mobile synchronization clients are not exercised because their +network behavior uses the same WebDAV endpoint and requires external client +systems. diff --git a/overlay/etc/apache2/sites-available/owncloud.conf b/overlay/etc/apache2/sites-available/owncloud.conf index f4a9db5..134fc15 100644 --- a/overlay/etc/apache2/sites-available/owncloud.conf +++ b/overlay/etc/apache2/sites-available/owncloud.conf @@ -11,15 +11,12 @@ ServerName localhost SSLEngine on ServerAdmin webmaster@localhost - DocumentRoot /var/www/owncloud/ + ProxyPreserveHost On + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + RequestHeader set X-Forwarded-Proto "https" Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains" - - - Options +FollowSymLinks - AllowOverride All - Require all granted - diff --git a/overlay/etc/cron.d/owncloud b/overlay/etc/cron.d/owncloud index c8b8754..479afba 100644 --- a/overlay/etc/cron.d/owncloud +++ b/overlay/etc/cron.d/owncloud @@ -1,2 +1,2 @@ # run cron.php for owncloud -*/15 * * * * www-data /usr/bin/php -f /var/www/owncloud/occ system:cron /dev/null 2>&1 +*/15 * * * * root /usr/local/bin/turnkey-occ system:cron >/dev/null 2>&1 diff --git a/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf b/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf new file mode 100644 index 0000000..872ed0c --- /dev/null +++ b/overlay/etc/systemd/system/mariadb.service.d/owncloud-network.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=owncloud-network.service +After=owncloud-network.service diff --git a/overlay/etc/systemd/system/owncloud-network.service b/overlay/etc/systemd/system/owncloud-network.service new file mode 100644 index 0000000..0d8d436 --- /dev/null +++ b/overlay/etc/systemd/system/owncloud-network.service @@ -0,0 +1,13 @@ +[Unit] +Description=Prepare the private ownCloud container network +Requires=docker.service +After=docker.service +Before=mariadb.service redis-server.service owncloud.service + +[Service] +Type=oneshot +ExecStart=/usr/local/lib/owncloud/network-ready +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/overlay/etc/systemd/system/owncloud.service b/overlay/etc/systemd/system/owncloud.service new file mode 100644 index 0000000..ef38ce7 --- /dev/null +++ b/overlay/etc/systemd/system/owncloud.service @@ -0,0 +1,15 @@ +[Unit] +Description=ownCloud Server container +Requires=docker.service owncloud-network.service mariadb.service redis-server.service +After=docker.service owncloud-network.service mariadb.service redis-server.service + +[Service] +Type=simple +ExecStartPre=/usr/local/lib/owncloud/wait-firstboot +ExecStart=/usr/local/lib/owncloud/run +ExecStop=-/usr/bin/docker stop --time 30 owncloud_server +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf b/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf new file mode 100644 index 0000000..872ed0c --- /dev/null +++ b/overlay/etc/systemd/system/redis-server.service.d/owncloud-network.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=owncloud-network.service +After=owncloud-network.service diff --git a/overlay/usr/lib/inithooks/bin/owncloud.py b/overlay/usr/lib/inithooks/bin/owncloud.py index b7bc7d2..e1bbfdd 100755 --- a/overlay/usr/lib/inithooks/bin/owncloud.py +++ b/overlay/usr/lib/inithooks/bin/owncloud.py @@ -9,10 +9,9 @@ import sys import getopt +import os import subprocess -from subprocess import call -from os.path import * -from os import chdir +import time from libinithooks.dialog_wrapper import Dialog @@ -62,18 +61,44 @@ def main(): if domain == "DEFAULT": domain = DEFAULT_DOMAIN - sedcom = """ - /0 => 'localhost',/ a\ - 1 => '%s', - """ - - conf = '/var/www/owncloud/config/config.php' - call(['sed', '-i', "/1 => /d", conf]) - call(['sed', '-i', sedcom % domain, conf]) - - call(['/usr/local/bin/turnkey-occ', 'user:resetpassword', '--password-from-env admin'], - cwd='/var/www/owncloud', - env={"OC_PASS": password}) + occ = '/usr/local/bin/turnkey-occ' + for _ in range(150): + status = subprocess.run([occ, 'status'], check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + if status.returncode == 0: + break + time.sleep(2) + else: + raise RuntimeError('ownCloud did not become ready within 300 seconds') + + env = os.environ.copy() + env['OC_PASS'] = password + subprocess.run([occ, 'user:resetpassword', '--password-from-env', 'admin'], + check=True, env=env) + subprocess.run([occ, 'config:system:set', 'trusted_domains', '1', + f'--value={domain}'], check=True) + subprocess.run([occ, 'config:system:set', 'overwrite.cli.url', + f'--value=https://{domain}'], check=True) + + env_path = '/etc/owncloud/owncloud.env' + with open(env_path, encoding='utf-8') as source: + lines = source.readlines() + replacements = { + 'OWNCLOUD_DOMAIN': domain, + 'OWNCLOUD_TRUSTED_DOMAINS': f'localhost,127.0.0.1,{domain}', + 'OWNCLOUD_OVERWRITE_CLI_URL': f'https://{domain}', + } + with open(env_path, 'w', encoding='utf-8') as target: + for line in lines: + key = line.partition('=')[0] + if key == 'OWNCLOUD_ADMIN_PASSWORD': + continue + if key in replacements: + target.write(f'{key}={replacements[key]}\n') + else: + target.write(line) + os.chmod(env_path, 0o600) if __name__ == "__main__": diff --git a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets index 9d65116..61d6720 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets @@ -1,19 +1,20 @@ -#!/bin/bash -e -# regenerate owncloud salt and mysql password +#!/bin/bash +set -Eeuo pipefail +# Regenerate the database and Redis credentials before ownCloud starts. . /etc/default/inithooks -CONF=/var/www/owncloud/config/config.php - -SALT=$(mcookie) -sed -i "s|passwordsalt.*|passwordsalt' => '$SALT',|" $CONF +PASSWORD=$(mcookie) +REDIS_PASSWORD=$(mcookie) -INSTANCEID=$(mcookie | cut -b -13) -sed -i "s|instanceid.*|instanceid' => '$INSTANCEID',|" $CONF +mysql --batch --execute "ALTER USER 'owncloud'@'172.28.0.%' IDENTIFIED BY '$PASSWORD'; FLUSH PRIVILEGES;" +sed -i "s/^OWNCLOUD_DB_PASSWORD=.*/OWNCLOUD_DB_PASSWORD=$PASSWORD/" \ + /etc/owncloud/owncloud.env -PASSWORD=$(mcookie) -sed -i "s|dbpassword.*|dbpassword' => '$PASSWORD',|" $CONF -$INITHOOKS_PATH/bin/mysqlconf.py --user=owncloud --pass="$PASSWORD" +sed -i "s/^requirepass .*/requirepass $REDIS_PASSWORD/" \ + /etc/redis/redis.conf +sed -i "s/^OWNCLOUD_REDIS_PASSWORD=.*/OWNCLOUD_REDIS_PASSWORD=$REDIS_PASSWORD/" \ + /etc/owncloud/owncloud.env +systemctl restart redis-server.service -SECRET=$(mcookie)$(mcookie)$(mcookie) -sed -i "s|secret.*|secret' => '$SECRET',|" $CONF +touch /etc/owncloud/firstboot-ready diff --git a/overlay/usr/local/bin/turnkey-occ b/overlay/usr/local/bin/turnkey-occ index 9cbc3c0..3f5950e 100755 --- a/overlay/usr/local/bin/turnkey-occ +++ b/overlay/usr/local/bin/turnkey-occ @@ -1,10 +1,11 @@ -#!/bin/bash -e +#!/bin/bash +set -Eeuo pipefail -[[ -z "$DEBUG" ]] || set -x +[[ -z ${DEBUG:-} ]] || set -x -OC_USR=${OC_USR:-www-data} -OCC=/var/www/owncloud/occ +args=(--user "${OC_USR:-www-data}") +if [[ -n ${OC_PASS:-} ]]; then + args+=(--env "OC_PASS=$OC_PASS") +fi -COMMAND="/usr/bin/php $OCC $@" - -runuser $OC_USR -s /bin/bash -c "$COMMAND" +exec docker exec "${args[@]}" owncloud_server occ "$@" diff --git a/overlay/usr/local/lib/owncloud/network-ready b/overlay/usr/local/lib/owncloud/network-ready new file mode 100755 index 0000000..916b1d0 --- /dev/null +++ b/overlay/usr/local/lib/owncloud/network-ready @@ -0,0 +1,17 @@ +#!/bin/bash +set -Eeuo pipefail + +network=owncloud +subnet=172.28.0.0/24 +gateway=172.28.0.1 + +if docker network inspect "$network" >/dev/null 2>&1; then + actual_subnet=$(docker network inspect --format \ + '{{(index .IPAM.Config 0).Subnet}}' "$network") + actual_gateway=$(docker network inspect --format \ + '{{(index .IPAM.Config 0).Gateway}}' "$network") + [[ $actual_subnet == "$subnet" && $actual_gateway == "$gateway" ]] +else + docker network create --driver bridge --subnet "$subnet" \ + --gateway "$gateway" "$network" >/dev/null +fi diff --git a/overlay/usr/local/lib/owncloud/run b/overlay/usr/local/lib/owncloud/run new file mode 100755 index 0000000..2b537ab --- /dev/null +++ b/overlay/usr/local/lib/owncloud/run @@ -0,0 +1,18 @@ +#!/bin/bash +set -Eeuo pipefail + +source /etc/owncloud/image.conf + +if ! docker image inspect "$OWNCLOUD_IMAGE" >/dev/null 2>&1; then + archive=/usr/local/src/owncloud-server.tar + [[ -s $archive ]] + docker load --input "$archive" >/dev/null + docker image inspect "$OWNCLOUD_IMAGE" >/dev/null + truncate --size 0 "$archive" +fi + +exec docker run --rm --name owncloud_server --network owncloud \ + --publish 127.0.0.1:8080:8080 \ + --env-file /etc/owncloud/owncloud.env \ + --volume /var/lib/owncloud:/mnt/data \ + "$OWNCLOUD_IMAGE" diff --git a/overlay/usr/local/lib/owncloud/wait-firstboot b/overlay/usr/local/lib/owncloud/wait-firstboot new file mode 100755 index 0000000..a8acd8e --- /dev/null +++ b/overlay/usr/local/lib/owncloud/wait-firstboot @@ -0,0 +1,14 @@ +#!/bin/bash +set -Eeuo pipefail + +source /etc/default/inithooks + +[[ ${RUN_FIRSTBOOT:-true} == false ]] && exit 0 + +for _ in $(seq 1 150); do + [[ -e /etc/owncloud/firstboot-ready ]] && exit 0 + sleep 2 +done + +echo 'ownCloud firstboot credentials were not prepared within 300 seconds' >&2 +exit 1 diff --git a/overlay/usr/local/sbin/owncloud-update b/overlay/usr/local/sbin/owncloud-update new file mode 100755 index 0000000..c045d0b --- /dev/null +++ b/overlay/usr/local/sbin/owncloud-update @@ -0,0 +1,58 @@ +#!/bin/bash +set -Eeuo pipefail + +usage() { + echo 'Usage: owncloud-update --check VERSION' >&2 + echo ' owncloud-update VERSION sha256:DIGEST' >&2 + exit 1 +} + +candidate_digest() { + skopeo inspect --raw "docker://docker.io/owncloud/server:$1" | \ + python3 -c ' +import json +import sys + +manifest = json.load(sys.stdin) +for item in manifest.get("manifests", []): + platform = item.get("platform", {}) + if platform.get("os") == "linux" and platform.get("architecture") == "amd64": + print(item["digest"]) + break +else: + raise SystemExit("official tag has no linux/amd64 manifest") +' +} + +if [[ ${1:-} == --check ]]; then + [[ $# == 2 ]] || usage + digest=$(candidate_digest "$2") + printf 'version=%s\ndigest=%s\nsource=docker.io/owncloud/server\n' \ + "$2" "$digest" + exit 0 +fi + +[[ $# == 2 ]] || usage +version=$1 +digest=$2 +[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] +[[ $digest =~ ^sha256:[0-9a-f]{64}$ ]] + +current=$(candidate_digest "$version") +if [[ $current != "$digest" ]]; then + echo "official amd64 digest is $current, not $digest" >&2 + exit 1 +fi + +docker pull "owncloud/server@$digest" +suffix=${digest#sha256:} +local_tag="owncloud/server:${version}-turnkey-${suffix:0:12}" +docker tag "owncloud/server@$digest" "$local_tag" + +cat > /etc/owncloud/image.conf < -#include - -php7.4-gd -php7.4-cli -php7.4-curl -php7.4-json -php7.4-intl -php7.4-imagick -php7.4-zip -php7.4-mbstring -php-pear -php7.4-xml +#include +docker.io +skopeo redis-server -php7.4-igbinary -php7.4-redis - -smbclient -php7.4-ldap -php7.4-imap -php7.4-gmp - -bzip2 -ffmpeg diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..9c2f5c3 --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,179 @@ +#!/bin/bash +set -Eeuo pipefail +umask 077 + +result=${TKL_TEST_RESULT:?TKL_TEST_RESULT is required} +app_password=${TKL_TEST_APP_PASS:?TKL_TEST_APP_PASS is required} +db_password=${TKL_TEST_DB_PASS:?TKL_TEST_DB_PASS is required} +base=https://localhost +remote_file=tkl-v19-acceptance-$$.txt +payload_one=/tmp/tkl-owncloud-payload-one.$$ +payload_two=/tmp/tkl-owncloud-payload-two.$$ +response=/tmp/tkl-owncloud-response.$$ +headers=/tmp/tkl-owncloud-headers.$$ +adminer_cookies=/tmp/tkl-owncloud-adminer-cookies.$$ +update_check=/tmp/tkl-owncloud-update.$$ +policy=/tmp/tkl-owncloud-policy.$$ +test_user=tkl-v19-user-$$ + +report_error() { + printf 'test_failure line=%s status=%s command=%q\n' \ + "$1" "$2" "$3" >&2 + exit "$2" +} +trap 'report_error "$LINENO" "$?" "$BASH_COMMAND"' ERR + +cleanup() { + curl --insecure --silent --user "admin:$app_password" --request DELETE \ + "$base/remote.php/dav/files/admin/$remote_file" >/dev/null 2>&1 || true + OC_PASS=$app_password turnkey-occ user:delete "$test_user" >/dev/null 2>&1 || true + rm -f -- "$payload_one" "$payload_two" "$response" "$headers" \ + "$adminer_cookies" "$update_check" "$policy" +} +trap cleanup EXIT + +systemctl --quiet is-active docker.service owncloud-network.service \ + owncloud.service apache2.service mariadb.service redis-server.service \ + postfix.service multi-user.target +systemctl --quiet is-enabled docker.service owncloud-network.service \ + owncloud.service apache2.service mariadb.service redis-server.service \ + postfix.service +apache2ctl -t +apache2ctl -M 2>/dev/null | grep -F ' proxy_module ' >/dev/null +apache2ctl -M 2>/dev/null | grep -F ' proxy_http_module ' >/dev/null + +owncloud_version=$(turnkey-occ status | awk '/versionstring:/ {print $3}') +test "$owncloud_version" = 11.0.0 +grep -Fxq 'version=11.0.0' /etc/owncloud/image-source +grep -Fxq \ + 'digest=sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366' \ + /etc/owncloud/image-source +source /etc/owncloud/image.conf +test "$OWNCLOUD_VERSION" = 11.0.0 +test "$OWNCLOUD_DIGEST" = \ + sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366 +test "$(docker inspect --format '{{.Config.Image}}' owncloud_server)" = \ + "$OWNCLOUD_IMAGE" +test "$(docker inspect --format '{{.State.Running}}' owncloud_server)" = true +test "$(docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}' owncloud_server)" = owncloud +test "$(docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}' owncloud)" = 172.28.0.0/24 +test "$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' owncloud)" = 172.28.0.1 + +curl --insecure --silent --show-error --dump-header "$headers" \ + --output /dev/null http://localhost/ +grep -Eq '^HTTP/.* 301' "$headers" +grep -Eqi '^location: https://localhost/' "$headers" +curl --insecure --fail --silent --show-error "$base/status.php" >"$response" +grep -q '"installed":true' "$response" +grep -q '"versionstring":"11.0.0"' "$response" +curl --insecure --fail --silent --show-error "$base/" >"$response" +grep -qi 'owncloud' "$response" + +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/ocs/v1.php/cloud/user?format=json" >"$response" +grep -q '"statuscode":100' "$response" +grep -q '"id":"admin"' "$response" + +printf 'ownCloud v19 first payload\n' >"$payload_one" +printf 'ownCloud v19 updated payload\n' >"$payload_two" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --upload-file "$payload_one" \ + "$base/remote.php/dav/files/admin/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/remote.php/dav/files/admin/$remote_file" >"$response" +cmp "$payload_one" "$response" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --upload-file "$payload_two" \ + "$base/remote.php/dav/files/admin/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + "$base/remote.php/dav/files/admin/$remote_file" >"$response" +cmp "$payload_two" "$response" +mariadb --batch --skip-column-names owncloud \ + --execute="SELECT path FROM oc_filecache WHERE name='$remote_file';" | \ + grep -F "/$remote_file" +curl --insecure --fail --silent --show-error --user "admin:$app_password" \ + --request DELETE "$base/remote.php/dav/files/admin/$remote_file" +if curl --insecure --silent --user "admin:$app_password" --output /dev/null \ + --write-out '%{http_code}' \ + "$base/remote.php/dav/files/admin/$remote_file" | grep -vq '^404$'; then + echo 'deleted WebDAV file remained accessible' >&2 + exit 1 +fi + +OC_PASS=$app_password turnkey-occ user:add --password-from-env "$test_user" +turnkey-occ user:list | grep -F "$test_user" +mariadb --batch --skip-column-names owncloud \ + --execute="SELECT uid FROM oc_users WHERE uid='$test_user';" | \ + grep -Fx "$test_user" +turnkey-occ user:delete "$test_user" + +ss -ltn | grep -Eq '127\.0\.0\.1:8080[[:space:]]' +ss -ltn | grep -Eq '172\.28\.0\.1:3306[[:space:]]' +ss -ltn | grep -Eq '172\.28\.0\.1:6379[[:space:]]' +if ss -ltn | grep -Eq '(0\.0\.0\.0|\[::\]):(3306|6379|8080)[[:space:]]'; then + echo 'an ownCloud backend unexpectedly listens on every interface' >&2 + exit 1 +fi +redis_password=$(sed -n 's/^OWNCLOUD_REDIS_PASSWORD=//p' \ + /etc/owncloud/owncloud.env) +redis-cli --host 172.28.0.1 --pass "$redis_password" ping 2>/dev/null | \ + grep -Fx PONG +if redis-cli --host 172.28.0.1 ping 2>&1 | grep -Fq PONG; then + echo 'Redis accepted an unauthenticated request' >&2 + exit 1 +fi + +curl --insecure --fail --silent --show-error \ + https://127.0.0.1:12322/ >"$response" +grep -qi 'Adminer' "$response" +curl --insecure --silent --show-error --location \ + --cookie-jar "$adminer_cookies" --cookie "$adminer_cookies" \ + --data-urlencode 'auth[driver]=server' \ + --data-urlencode 'auth[server]=localhost' \ + --data-urlencode 'auth[username]=adminer' \ + --data-urlencode "auth[password]=$db_password" \ + --data-urlencode 'auth[db]=owncloud' \ + https://127.0.0.1:12322/ >"$response" +grep -qi 'owncloud' "$response" +grep -qi 'Logout' "$response" +if grep -qi 'Invalid credentials\|Access denied' "$response"; then + echo 'Adminer rejected its firstboot MariaDB credentials' >&2 + exit 1 +fi + +docker_image_before=$(docker image inspect --format '{{.Id}}' "$OWNCLOUD_IMAGE") +owncloud-update --check 11.0.0 >"$update_check" +grep -Fxq 'version=11.0.0' "$update_check" +grep -Eq '^digest=sha256:[0-9a-f]{64}$' "$update_check" +grep -Fxq 'source=docker.io/owncloud/server' "$update_check" +test "$(docker image inspect --format '{{.Id}}' "$OWNCLOUD_IMAGE")" = \ + "$docker_image_before" + +apache_version=$(dpkg-query -W -f='${Version}' apache2) +mariadb_version=$(dpkg-query -W -f='${Version}' mariadb-server) +redis_version=$(dpkg-query -W -f='${Version}' redis-server) +docker_version=$(dpkg-query -W -f='${Version}' docker.io) +skopeo_version=$(dpkg-query -W -f='${Version}' skopeo) +before="$apache_version|$mariadb_version|$redis_version|$docker_version|$skopeo_version" +apt-get update >/dev/null +for package in apache2 mariadb-server redis-server docker.io skopeo; do + apt-cache policy "$package" >"$policy" + candidate=$(awk '/Candidate:/ {print $2}' "$policy") + test -n "$candidate" + test "$candidate" != '(none)' + grep -Eq 'trixie|deb13' "$policy" +done +after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' skopeo)" +test "$after" = "$before" +grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d +! grep -Rqi bookworm /etc/apt/sources.list.d + +cat >"$result" < Date: Mon, 24 Aug 2026 13:58:33 +0000 Subject: [PATCH 02/12] Install the Trixie Docker client Debian Trixie splits the Docker daemon and command-line client. FAB omits recommended packages, so docker.io alone left the daemon active but every appliance management command unavailable. Install docker-cli explicitly. Create the custom service enablement links directly because FAB's build-time systemctl shim did not enable the native ownCloud units. This lets firstboot create the private bridge before MariaDB and Redis, then start the ownCloud container automatically. Verified in failed exact run 20260824t130813z-2903-21758: after installing docker-cli in the disposable runtime, the bridge, MariaDB, Redis, credential rotation, Adminer configuration and ownCloud image load all started successfully. Shell syntax and git diff checks pass. --- conf.d/main | 7 ++++++- plan/main | 1 + tests/v19.sh | 13 +++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/conf.d/main b/conf.d/main index 6ed3586..2f06f1a 100755 --- a/conf.d/main +++ b/conf.d/main @@ -63,4 +63,9 @@ sed -i "s/^# requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf service mysql stop service apache2 stop -systemctl enable docker.service owncloud-network.service owncloud.service +systemctl enable docker.service +install -d /etc/systemd/system/multi-user.target.wants +ln -s ../owncloud-network.service \ + /etc/systemd/system/multi-user.target.wants/owncloud-network.service +ln -s ../owncloud.service \ + /etc/systemd/system/multi-user.target.wants/owncloud.service diff --git a/plan/main b/plan/main index 1b16138..ff4e3e9 100644 --- a/plan/main +++ b/plan/main @@ -2,5 +2,6 @@ #include docker.io +docker-cli skopeo redis-server diff --git a/tests/v19.sh b/tests/v19.sh index 9c2f5c3..07f647c 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -153,26 +153,27 @@ apache_version=$(dpkg-query -W -f='${Version}' apache2) mariadb_version=$(dpkg-query -W -f='${Version}' mariadb-server) redis_version=$(dpkg-query -W -f='${Version}' redis-server) docker_version=$(dpkg-query -W -f='${Version}' docker.io) +docker_cli_version=$(dpkg-query -W -f='${Version}' docker-cli) skopeo_version=$(dpkg-query -W -f='${Version}' skopeo) -before="$apache_version|$mariadb_version|$redis_version|$docker_version|$skopeo_version" +before="$apache_version|$mariadb_version|$redis_version|$docker_version|$docker_cli_version|$skopeo_version" apt-get update >/dev/null -for package in apache2 mariadb-server redis-server docker.io skopeo; do +for package in apache2 mariadb-server redis-server docker.io docker-cli skopeo; do apt-cache policy "$package" >"$policy" candidate=$(awk '/Candidate:/ {print $2}' "$policy") test -n "$candidate" test "$candidate" != '(none)' grep -Eq 'trixie|deb13' "$policy" done -after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' skopeo)" +after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' docker-cli)|$(dpkg-query -W -f='${Version}' skopeo)" test "$after" = "$before" grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d ! grep -Rqi bookworm /etc/apt/sources.list.d cat >"$result" < Date: Mon, 24 Aug 2026 14:45:06 +0000 Subject: [PATCH 03/12] Wait for MariaDB and avoid Docker vfs Firstboot raced MariaDB's four-second startup and attempted credential rotation before its local socket existed. Wait up to 60 seconds for a successful MariaDB ping before changing the application account. The exact overlay-backed runtime rejected overlay2 and lacked fuse-overlayfs, so Docker fell back to vfs. Loading the 2.21 GB ownCloud image remained incomplete through the 240-second health deadline. Install Debian's fuse-overlayfs package and require an overlay-based driver in focused acceptance. Verified against failed exact run 20260824t135841z-4194-13598 and its disposable continuation: the private network, enabled units, MariaDB, Redis and Adminer all started, while the measured vfs image load exceeded the runtime deadline. Shell syntax, documentation character and git diff checks pass. --- docs/v19.0-testing.md | 9 ++++++--- .../firstboot.d/20regen-owncloud-secrets | 6 ++++++ plan/main | 1 + tests/v19.sh | 20 ++++++++++++------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 18d4f37..907f026 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -12,8 +12,9 @@ it next opens its SQLite database on PHP 8.4. The appliance therefore uses the official ownCloud Server 11.0.0 image for amd64, pinned to manifest digest `sha256:dbebc24fe77a35c5de621d38a3f7264ffb43e3ee321928cde9a563a72b8ff366`. -Skopeo retrieves and verifies that digest during the build. Docker, Skopeo, -Apache, MariaDB, Redis and Adminer come from signed Debian Trixie repositories. +Skopeo retrieves and verifies that digest during the build. Docker, its +overlay-filesystem fallback, Skopeo, Apache, MariaDB, Redis and Adminer come +from signed Debian Trixie repositories. The ownCloud container joins a dedicated `172.28.0.0/24` bridge. Debian MariaDB and Redis listen only on its gateway, `172.28.0.1`, and Redis requires @@ -83,7 +84,9 @@ Pending the exact acceptance command. The Docker acceptance path does not exercise the installer, kernel, bootloader or physical hardware. It does exercise the appliance's nested Docker daemon in the privileged systemd runtime, the same services and -network layout used on a normal installed appliance. +network layout used on a normal installed appliance. The nested runtime uses +`fuse-overlayfs` because its root is already overlay-backed; a normal appliance +installed on a supported filesystem can use Docker's `overlay2` driver. ownCloud Server 11 supports only container deployment. Administrators who add applications requiring extra operating-system binaries must derive and pin a diff --git a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets index 61d6720..575e5a5 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20regen-owncloud-secrets @@ -4,6 +4,12 @@ set -Eeuo pipefail . /etc/default/inithooks +for _ in $(seq 1 60); do + mariadb-admin ping --silent >/dev/null 2>&1 && break + sleep 1 +done +mariadb-admin ping --silent >/dev/null + PASSWORD=$(mcookie) REDIS_PASSWORD=$(mcookie) diff --git a/plan/main b/plan/main index ff4e3e9..0e16ba2 100644 --- a/plan/main +++ b/plan/main @@ -3,5 +3,6 @@ docker.io docker-cli +fuse-overlayfs skopeo redis-server diff --git a/tests/v19.sh b/tests/v19.sh index 07f647c..90defb2 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -58,6 +58,11 @@ test "$(docker inspect --format '{{.State.Running}}' owncloud_server)" = true test "$(docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}' owncloud_server)" = owncloud test "$(docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}' owncloud)" = 172.28.0.0/24 test "$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' owncloud)" = 172.28.0.1 +docker_storage_driver=$(docker info --format '{{.Driver}}') +case "$docker_storage_driver" in + overlay2|fuse-overlayfs) ;; + *) echo "unexpected Docker storage driver: $docker_storage_driver" >&2; exit 1;; +esac curl --insecure --silent --show-error --dump-header "$headers" \ --output /dev/null http://localhost/ @@ -154,26 +159,27 @@ mariadb_version=$(dpkg-query -W -f='${Version}' mariadb-server) redis_version=$(dpkg-query -W -f='${Version}' redis-server) docker_version=$(dpkg-query -W -f='${Version}' docker.io) docker_cli_version=$(dpkg-query -W -f='${Version}' docker-cli) +fuse_overlayfs_version=$(dpkg-query -W -f='${Version}' fuse-overlayfs) skopeo_version=$(dpkg-query -W -f='${Version}' skopeo) -before="$apache_version|$mariadb_version|$redis_version|$docker_version|$docker_cli_version|$skopeo_version" +before="$apache_version|$mariadb_version|$redis_version|$docker_version|$docker_cli_version|$fuse_overlayfs_version|$skopeo_version" apt-get update >/dev/null -for package in apache2 mariadb-server redis-server docker.io docker-cli skopeo; do +for package in apache2 mariadb-server redis-server docker.io docker-cli fuse-overlayfs skopeo; do apt-cache policy "$package" >"$policy" candidate=$(awk '/Candidate:/ {print $2}' "$policy") test -n "$candidate" test "$candidate" != '(none)' grep -Eq 'trixie|deb13' "$policy" done -after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' docker-cli)|$(dpkg-query -W -f='${Version}' skopeo)" +after="$(dpkg-query -W -f='${Version}' apache2)|$(dpkg-query -W -f='${Version}' mariadb-server)|$(dpkg-query -W -f='${Version}' redis-server)|$(dpkg-query -W -f='${Version}' docker.io)|$(dpkg-query -W -f='${Version}' docker-cli)|$(dpkg-query -W -f='${Version}' fuse-overlayfs)|$(dpkg-query -W -f='${Version}' skopeo)" test "$after" = "$before" grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d ! grep -Rqi bookworm /etc/apt/sources.list.d cat >"$result" < Date: Mon, 24 Aug 2026 15:20:38 +0000 Subject: [PATCH 04/12] Wait for ownCloud installation to finish ownCloud's occ status command exits successfully before the database installation completes, which let firstboot race into an unavailable user command and left Apache returning 502 responses. Require the installed status, wait for the internal HTTP endpoint, and record a configuration marker that acceptance now checks. This keeps normal firstboot completion aligned with application readiness. Verified with the retained exact-run failure log, Python and shell syntax checks, and git diff whitespace validation. --- overlay/usr/lib/inithooks/bin/owncloud.py | 25 ++++++++++++++++++++--- tests/v19.sh | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/overlay/usr/lib/inithooks/bin/owncloud.py b/overlay/usr/lib/inithooks/bin/owncloud.py index e1bbfdd..600bd74 100755 --- a/overlay/usr/lib/inithooks/bin/owncloud.py +++ b/overlay/usr/lib/inithooks/bin/owncloud.py @@ -9,9 +9,11 @@ import sys import getopt +import json import os import subprocess import time +import urllib.request from libinithooks.dialog_wrapper import Dialog @@ -64,9 +66,10 @@ def main(): occ = '/usr/local/bin/turnkey-occ' for _ in range(150): status = subprocess.run([occ, 'status'], check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL) - if status.returncode == 0: + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True) + if status.returncode == 0 and '- installed: true' in status.stdout: break time.sleep(2) else: @@ -100,6 +103,22 @@ def main(): target.write(line) os.chmod(env_path, 0o600) + for _ in range(150): + try: + with urllib.request.urlopen( + 'http://127.0.0.1:8080/status.php', timeout=2) as response: + status = json.load(response) + if status.get('installed') is True: + break + except (OSError, ValueError): + pass + time.sleep(2) + else: + raise RuntimeError('ownCloud HTTP endpoint was not ready within 300 seconds') + + with open('/etc/owncloud/configured', 'w', encoding='utf-8'): + pass + if __name__ == "__main__": main() diff --git a/tests/v19.sh b/tests/v19.sh index 90defb2..e0b8029 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -41,6 +41,7 @@ systemctl --quiet is-enabled docker.service owncloud-network.service \ apache2ctl -t apache2ctl -M 2>/dev/null | grep -F ' proxy_module ' >/dev/null apache2ctl -M 2>/dev/null | grep -F ' proxy_http_module ' >/dev/null +test -e /etc/owncloud/configured owncloud_version=$(turnkey-occ status | awk '/versionstring:/ {print $3}') test "$owncloud_version" = 11.0.0 From c076d8c84b9eba49b06794cfa3e7d7857e2bd636 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Mon, 24 Aug 2026 15:43:27 +0000 Subject: [PATCH 05/12] Follow the ownCloud login redirect in acceptance The ready ownCloud root endpoint redirects to its login route and returns no identifying body at the initial URL. The functional test therefore failed after firstboot and status checks had passed. Follow the normal browser redirect before asserting the login page content. Verified with the retained exact-run failure location, shell syntax, and git diff whitespace checks. --- tests/v19.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v19.sh b/tests/v19.sh index e0b8029..76d92ee 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -72,7 +72,7 @@ grep -Eqi '^location: https://localhost/' "$headers" curl --insecure --fail --silent --show-error "$base/status.php" >"$response" grep -q '"installed":true' "$response" grep -q '"versionstring":"11.0.0"' "$response" -curl --insecure --fail --silent --show-error "$base/" >"$response" +curl --insecure --fail --silent --show-error --location "$base/" >"$response" grep -qi 'owncloud' "$response" curl --insecure --fail --silent --show-error --user "admin:$app_password" \ From 5f283576bd5d0ef366f3b38b53b596b4c90bfa32 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Mon, 24 Aug 2026 16:10:39 +0000 Subject: [PATCH 06/12] Verify Redis through the ownCloud runtime The host-side authenticated redis-cli assertion failed even though ownCloud had already completed Redis-backed WebDAV operations, while a disposable Trixie probe confirmed the CLI syntax and output. Check that the server and application password sources agree, authenticate and ping through the pinned image's PHP Redis extension, and require an explicit unauthenticated rejection. This exercises the actual appliance trust path without weakening the credential boundary. Verified with shell syntax and whitespace checks plus disposable Trixie redis-cli and pinned-image PHP extension probes. --- tests/v19.sh | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/v19.sh b/tests/v19.sh index 76d92ee..678ac38 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -122,12 +122,17 @@ if ss -ltn | grep -Eq '(0\.0\.0\.0|\[::\]):(3306|6379|8080)[[:space:]]'; then fi redis_password=$(sed -n 's/^OWNCLOUD_REDIS_PASSWORD=//p' \ /etc/owncloud/owncloud.env) -redis-cli --host 172.28.0.1 --pass "$redis_password" ping 2>/dev/null | \ - grep -Fx PONG -if redis-cli --host 172.28.0.1 ping 2>&1 | grep -Fq PONG; then - echo 'Redis accepted an unauthenticated request' >&2 - exit 1 -fi +grep -Fxq "requirepass $redis_password" /etc/redis/redis.conf +docker exec owncloud_server php -r ' +$redis = new Redis(); +$redis->connect(getenv("OWNCLOUD_REDIS_HOST"), + (int) getenv("OWNCLOUD_REDIS_PORT")); +if (!$redis->auth(getenv("OWNCLOUD_REDIS_PASSWORD")) || !$redis->ping()) { + exit(1); +} +' +redis_unauthenticated=$(redis-cli --host 172.28.0.1 ping 2>&1 || true) +grep -Eq 'NOAUTH|Authentication required' <<<"$redis_unauthenticated" curl --insecure --fail --silent --show-error \ https://127.0.0.1:12322/ >"$response" From 6844886b08f2c024a93b5a72a7f8073e7ded45ce Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Mon, 24 Aug 2026 16:49:47 +0000 Subject: [PATCH 07/12] Assert Redis authentication by behavior Redis 8 authentication denial text and redis-cli exit status are not stable appliance contracts. Reject an exact unauthenticated PONG and emit a fixed denial classification while retaining the authenticated PHP Redis PONG check. Verified with Bash syntax checking and Debian Trixie Redis 8.0.2 authenticated and unauthenticated PING probes. --- tests/v19.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/v19.sh b/tests/v19.sh index 678ac38..6fe11d6 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -132,7 +132,11 @@ if (!$redis->auth(getenv("OWNCLOUD_REDIS_PASSWORD")) || !$redis->ping()) { } ' redis_unauthenticated=$(redis-cli --host 172.28.0.1 ping 2>&1 || true) -grep -Eq 'NOAUTH|Authentication required' <<<"$redis_unauthenticated" +if [[ "$redis_unauthenticated" == PONG ]]; then + echo 'Redis accepted an unauthenticated PING' >&2 + exit 1 +fi +echo 'redis_unauthenticated_ping=denied' curl --insecure --fail --silent --show-error \ https://127.0.0.1:12322/ >"$response" From 35c6120ba80408731f97dd2c9a8fe835fee0edec Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 25 Aug 2026 21:25:06 +0000 Subject: [PATCH 08/12] Verify Redis denial at the protocol boundary The retained acceptance run reached the unauthenticated Redis check after ownCloud login, WebDAV, user, database, and authenticated Redis behavior passed, but redis-cli did not expose denial through the assumed text and exit contract. Send a raw RESP PING instead and require Redis's NOAUTH error code. This preserves both reachability and authentication-denial criteria without depending on redis-cli presentation. Bash syntax, embedded Python compilation, retained evidence digests, and positive and negative response fixtures pass. --- tests/v19.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/v19.sh b/tests/v19.sh index 6fe11d6..33c5b2c 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -131,11 +131,15 @@ if (!$redis->auth(getenv("OWNCLOUD_REDIS_PASSWORD")) || !$redis->ping()) { exit(1); } ' -redis_unauthenticated=$(redis-cli --host 172.28.0.1 ping 2>&1 || true) -if [[ "$redis_unauthenticated" == PONG ]]; then - echo 'Redis accepted an unauthenticated PING' >&2 - exit 1 -fi +python3 - <<'PY' +import socket + +with socket.create_connection(('172.28.0.1', 6379), timeout=5) as redis: + redis.sendall(b'*1\r\n$4\r\nPING\r\n') + response = redis.recv(256) +if not response.startswith(b'-NOAUTH '): + raise SystemExit('Redis did not reject unauthenticated PING: %r' % response) +PY echo 'redis_unauthenticated_ping=denied' curl --insecure --fail --silent --show-error \ From e14b3dd333d5b3d36e76fb86ddad3ff870f93769 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 02:23:00 +0000 Subject: [PATCH 09/12] Prove OwnCloud cron and persistence boundaries The v19 acceptance flow covered the primary application behavior but did not explicitly exercise the shipped cron command or verify the persistent data mount described by the README. Add focused service, schedule, background-job, command, and mount assertions, and map the cron contract in the testing guide. This changes acceptance evidence only; appliance runtime behavior remains unchanged. Bash syntax, Python syntax, Docker template parsing, the installed cron line, and diff hygiene were verified before committing. --- docs/v19.0-testing.md | 4 +++- tests/v19.sh | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 907f026..b746061 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -28,6 +28,7 @@ and user data are stored under `/var/lib/owncloud`. TKLDEV_CONTAINER=tkldev19-wave2 \ TKL_HARNESS_STATE_DIR=/home/agent/.local/state/turnkey-v19-harness-wave2 \ TKL_HARNESS_LOCK_FILE=/home/agent/.local/state/turnkey-v19-harness-wave2/build.lock \ +TKL_HARNESS_DOCKER_LIMIT_BYTES=42949672960 \ /sandboxed-git/turnkey/tools/test-v19-appliance owncloud \ --source /home/agent/.local/worktrees/turnkey-apps/owncloud/wish-owncloud-v19-trixie ``` @@ -45,8 +46,9 @@ are recorded below after exact acceptance passes. | The firstboot `admin` credential controls ownCloud | Request the authenticated OCS user endpoint | The response identifies the `admin` account | `tests/v19.sh` | | Users can store and retrieve files | Create, read, update and delete a file through WebDAV, with MariaDB readback | Each content comparison passes, the database records the file and the deleted path returns HTTP 404 | `tests/v19.sh` | | `turnkey-occ` provides command-line administration as `www-data` | Create, list and delete a temporary user through the wrapper | The user appears in both `occ` and MariaDB before deletion | `tests/v19.sh` | -| Persistent data is outside the application image | Inspect the running container and its `/mnt/data` mount | The official image runs with `/var/lib/owncloud` mounted at `/mnt/data` | `tests/v19.sh` and harness runtime evidence | +| Persistent data is outside the application image | Inspect the running container and its `/mnt/data` mount | The official image runs with `/var/lib/owncloud` mounted at `/mnt/data` | `tests/v19.sh` | | MariaDB and Redis are private application backends | Inspect listeners, Docker network addressing and Redis authentication | Both services listen only on the private bridge gateway; Redis rejects unauthenticated requests | `tests/v19.sh` | +| Background jobs run through cron | Inspect the installed schedule and OwnCloud background-job mode, then execute the scheduled command | The cron service and schedule are enabled, the mode is `cron` and `turnkey-occ system:cron` succeeds | `tests/v19.sh` | | Adminer manages MariaDB over HTTPS on port 12322 | Submit the firstboot `adminer` login to the ownCloud database | Adminer accepts the credentials and exposes the ownCloud database | `tests/v19.sh` | | ownCloud and Debian components have maintained update channels | Query `owncloud-update --check` and refresh APT metadata without installing changes | The official image digest and eligible signed Trixie candidates are returned; installed versions and the running image remain unchanged | `tests/v19.sh` | | Webmin, SSH, Postfix, backup and other base services come from the inherited appliance layers | Exercise app-specific services and cite the unchanged Core and LAMP boundaries | Application services pass; inherited services retain their existing contracts | Core 19 and LAMP 19 acceptance reports | diff --git a/tests/v19.sh b/tests/v19.sh index 33c5b2c..12547a5 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -34,10 +34,10 @@ trap cleanup EXIT systemctl --quiet is-active docker.service owncloud-network.service \ owncloud.service apache2.service mariadb.service redis-server.service \ - postfix.service multi-user.target + cron.service postfix.service multi-user.target systemctl --quiet is-enabled docker.service owncloud-network.service \ owncloud.service apache2.service mariadb.service redis-server.service \ - postfix.service + cron.service postfix.service apache2ctl -t apache2ctl -M 2>/dev/null | grep -F ' proxy_module ' >/dev/null apache2ctl -M 2>/dev/null | grep -F ' proxy_http_module ' >/dev/null @@ -56,6 +56,8 @@ test "$OWNCLOUD_DIGEST" = \ test "$(docker inspect --format '{{.Config.Image}}' owncloud_server)" = \ "$OWNCLOUD_IMAGE" test "$(docker inspect --format '{{.State.Running}}' owncloud_server)" = true +test "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/mnt/data"}}{{.Source}}{{end}}{{end}}' owncloud_server)" = \ + /var/lib/owncloud test "$(docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{end}}' owncloud_server)" = owncloud test "$(docker network inspect --format '{{(index .IPAM.Config 0).Subnet}}' owncloud)" = 172.28.0.0/24 test "$(docker network inspect --format '{{(index .IPAM.Config 0).Gateway}}' owncloud)" = 172.28.0.1 @@ -113,6 +115,12 @@ mariadb --batch --skip-column-names owncloud \ grep -Fx "$test_user" turnkey-occ user:delete "$test_user" +grep -Fxq \ + '*/15 * * * * root /usr/local/bin/turnkey-occ system:cron >/dev/null 2>&1' \ + /etc/cron.d/owncloud +test "$(turnkey-occ config:app:get core backgroundjobs_mode)" = cron +turnkey-occ system:cron >/dev/null + ss -ltn | grep -Eq '127\.0\.0\.1:8080[[:space:]]' ss -ltn | grep -Eq '172\.28\.0\.1:3306[[:space:]]' ss -ltn | grep -Eq '172\.28\.0\.1:6379[[:space:]]' @@ -192,7 +200,7 @@ grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d cat >"$result" < Date: Wed, 26 Aug 2026 04:46:10 +0000 Subject: [PATCH 10/12] Record OwnCloud v19 acceptance evidence Replace the pending testing placeholder with the sealed exact-run evidence for the accepted Trixie candidate. Record the tested source and harness identities, installed versions, runtime coverage, updater behavior, report location, and PASS verdict so the migration claims remain auditable. The accepted runtime source remains e14b3dd333d5b3d36e76fb86ddad3ff870f93769; this commit changes documentation only. The retained report checksum bundle, criterion evidence, shell and Python syntax, documentation contract, and diff hygiene were verified. --- docs/v19.0-testing.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index b746061..2c3aaa2 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -79,7 +79,25 @@ than supported deployment paths. ## Accepted evidence -Pending the exact acceptance command. +Exact acceptance passed on 26 August 2026. + +- Run: `20260826t041224z-4757-16780` +- Tested source: `e14b3dd333d5b3d36e76fb86ddad3ff870f93769` +- Harness: `54ca2cff6b98a034089d186b249835b58fc467da` +- Report: `/home/agent/.local/state/turnkey-v19-harness-wave2/runs/owncloud/20260826t041224z-4757-16780/report.txt` +- Verdict: `PASS` +- Installed versions: ownCloud 11.0.0, Apache 2.4.68, + MariaDB 11.8.6, Redis 8.0.2, Docker 26.1.5, + fuse-overlayfs 1.14 and Skopeo 1.18.0 + +The run completed normal initialization and firstboot, authenticated the +administrator, exercised WebDAV create, read, update and delete with MariaDB +readback, created and deleted a user through `turnkey-occ`, ran the cron +background command, verified authenticated Redis access and unauthenticated +denial, logged in to Adminer and checked both update channels without changing +the running image or installed packages. The report records the persistent +data mount, private network, signed Trixie package metadata and pinned official +amd64 image digest. ## Known limitations From 15e205df0a866da126ce1c9a54fdb1ce8422da51 Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 05:05:31 +0000 Subject: [PATCH 11/12] Prevent bootstrap credentials from entering build logs The appliance configuration script enabled shell tracing while generating and interpolating MariaDB and Redis passwords, which exposed those values in retained acceptance logs. Run the script with error-exit behavior but without xtrace, and classify the README's contacts, calendar, and gallery examples as deferred from focused v19 coverage. Verified with Bash syntax checking, diff hygiene, and credential-pattern checks that do not emit secret values. --- conf.d/main | 2 +- docs/v19.0-testing.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/conf.d/main b/conf.d/main index 2f06f1a..ee43199 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,4 +1,4 @@ -#!/bin/bash -ex +#!/bin/bash -e DB_NAME=owncloud DB_USER=owncloud diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 2c3aaa2..5ba9c9b 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -45,6 +45,7 @@ are recorded below after exact acceptance passes. | ownCloud is available through the HTTPS web endpoint | Request `/` and `/status.php` through Apache | The login page and installed ownCloud 11 status are returned over HTTPS; HTTP redirects to HTTPS | `tests/v19.sh` | | The firstboot `admin` credential controls ownCloud | Request the authenticated OCS user endpoint | The response identifies the `admin` account | `tests/v19.sh` | | Users can store and retrieve files | Create, read, update and delete a file through WebDAV, with MariaDB readback | Each content comparison passes, the database records the file and the deleted path returns HTTP 404 | `tests/v19.sh` | +| The opening description names contacts, photo galleries and calendars | Classify these product-level examples against focused appliance coverage | Deferred; v19 acceptance covers the WebDAV file lifecycle, not CardDAV, CalDAV or the gallery interface | Deferred | | `turnkey-occ` provides command-line administration as `www-data` | Create, list and delete a temporary user through the wrapper | The user appears in both `occ` and MariaDB before deletion | `tests/v19.sh` | | Persistent data is outside the application image | Inspect the running container and its `/mnt/data` mount | The official image runs with `/var/lib/owncloud` mounted at `/mnt/data` | `tests/v19.sh` | | MariaDB and Redis are private application backends | Inspect listeners, Docker network addressing and Redis authentication | Both services listen only on the private bridge gateway; Redis rejects unauthenticated requests | `tests/v19.sh` | From 25cbaa7b69e8545494a0ae4c038eb671e253802a Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 06:12:27 +0000 Subject: [PATCH 12/12] Record sanitized OwnCloud acceptance evidence Replace the superseded evidence reference with the exact passing run for the credential-safe candidate. Record the tested source, report checksum, verified artifact count, and zero-match disclosure scan so review can bind the correction to retained evidence. Verified all eight entries in the retained SHA256SUMS bundle and confirmed the sealed corpus contains no generated database or Redis credential disclosures. --- docs/v19.0-testing.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 5ba9c9b..23c49c3 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -82,10 +82,12 @@ than supported deployment paths. Exact acceptance passed on 26 August 2026. -- Run: `20260826t041224z-4757-16780` -- Tested source: `e14b3dd333d5b3d36e76fb86ddad3ff870f93769` +- Run: `20260826t051749z-4089-14788` +- Tested source: `15e205df0a866da126ce1c9a54fdb1ce8422da51` - Harness: `54ca2cff6b98a034089d186b249835b58fc467da` -- Report: `/home/agent/.local/state/turnkey-v19-harness-wave2/runs/owncloud/20260826t041224z-4757-16780/report.txt` +- Report: `/home/agent/.local/state/turnkey-v19-harness-wave2/runs/owncloud/20260826t051749z-4089-14788/report.txt` +- Report SHA-256: `5f3f3c7e0b02071ee2e0b2551cadd2f9379f6591f65ac78abc3d9de9395a11ab` +- Retained evidence: all 8 checksums verified; credential-disclosure scan found 0 matches - Verdict: `PASS` - Installed versions: ownCloud 11.0.0, Apache 2.4.68, MariaDB 11.8.6, Redis 8.0.2, Docker 26.1.5,