diff --git a/README.rst b/README.rst index 7951856..820cb84 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,7 @@ and on top of that: - Syncthing: - - Installed from the `Stable Release Channel`_ via the official Syncthing - apt package repository. + - Syncthing 1.29 installed from Debian Trixie and maintained through APT. - Pre-configured for remote access, with password set on firstboot. @@ -33,7 +32,8 @@ Supervised Manual Syncthing Update To upgrade to the latest version of Syncthing from the command line:: apt-get update - apt-get install syncthing + apt-get install --only-upgrade syncthing + systemctl restart syncthing@syncthing.service Credentials *(passwords set at first boot)* @@ -45,4 +45,3 @@ Credentials *(passwords set at first boot)* .. _Syncthing: https://syncthing.net/ .. _TurnKey Core: https://www.turnkeylinux.org/core -.. _Stable Release Channel: https://docs.syncthing.net/users/releases.html diff --git a/changelog b/changelog index 4166fbf..d8483bc 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,17 @@ +turnkey-syncthing-19.0 (1) turnkey; urgency=low + + * Upgrade the base distribution to Debian 13/Trixie. + + * Use the Debian Trixie Syncthing package and signed APT update path instead + of the third-party Syncthing repository. + + * Preserve the firstboot-authenticated web interface, Nginx TLS proxy, + default synchronization folder and supervised update workflow. + + * See the TurnKey Core 19.0 changelog for changes common to all appliances. + + -- TurnKey Linux release engineering Mon, 24 Aug 2026 00:00:00 +0000 + turnkey-syncthing-18.0 (1) turnkey; urgency=low * Update to latest upstream Syncthing (via upstream apt repo) - v1.27.4. diff --git a/conf.d/main b/conf.d/main index c69abe4..2e675d7 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,11 +1,6 @@ #!/bin/sh -ex -APT_GPG_URL=https://syncthing.net/release-key.txt -curl -sS $APT_GPG_URL | apt-key --keyring /usr/share/keyrings/syncthing.gpg add - - useradd -r syncthing -m -d /home/syncthing -DEBIAN_FRONTEND=noninteractive apt-get update -y -DEBIAN_FRONTEND=noninteractive apt-get install -y syncthing # syncthing service generates secrets on first run, so is enabled and # started via /usr/lib/inithooks/firstboot.d/20regen-syncthing-secrets diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..64f11d2 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,82 @@ +# Syncthing 19.0 testing + +## Scope and source decision + +Syncthing 19.0 uses Debian 13 Trixie packages for Syncthing 1.29, Nginx and +Python bcrypt. Trixie provides the complete documented appliance stack, so v19 +removes the third-party Syncthing repository, its package preference and its +build-time key download. Signed Debian and TurnKey APT repositories provide the +maintained update path. + +The Debian package supplies the `syncthing@.service` systemd template. The +appliance runs that service as `syncthing`, regenerates its device identity and +GUI credentials at first boot, binds the native GUI to localhost, and exposes +it through the existing Nginx TLS proxy. + +## Acceptance command + +```sh +/sandboxed-git/turnkey/tools/test-v19-appliance syncthing \ + --source /home/agent/.local/worktrees/turnkey-apps/syncthing/wish-syncthing-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`. + +Accepted run: + +- Run: `20260824t121432z-6919-1904` +- Behavioral source: `ad29d6938160eda134545959137a702b9afdd02a` +- Report: `/home/agent/.local/state/turnkey-v19-harness/runs/syncthing/20260824t121432z-6919-1904/report.txt` +- Report SHA256: `4696f0686439032ae23fca06585dc5062bbb1691f4736742a5a1ab4bbe790a06` +- `SHA256SUMS` SHA256: `7eb650d7f7e05b4d407a5f4f6ff5279805692892f4a37301d13f5ace342b0f96` +- Verdict: `PASS` + +The accepted image installed Syncthing `1.29.5~ds1-2`, Nginx +`1.26.3-3+deb13u7` and Python bcrypt `4.2.0-2.1+b1`. Normal boot completed, +the firstboot hooks configured the application, and every crosswalk check +below passed. + +## README crosswalk + +| README contract | Focused check | Required result | Evidence | +| --- | --- | --- | --- | +| Syncthing 1.29 comes from Debian Trixie | Query package version and binary ownership, then inspect the APT candidate | The binary is Debian-owned and retains an eligible signed Trixie candidate | `tests/v19.sh` | +| Firstboot configures remote GUI credentials | Submit the real Syncthing password-login request and use its session cookie | The configured `syncthing` user receives an authenticated GUI session | `tests/v19.sh` | +| Syncthing runs as its dedicated service identity | Check the active and enabled template service, effective user, config ownership and listening sockets | Normal boot runs Syncthing as `syncthing`; its config and sync folder are owned by that account | `tests/v19.sh` | +| The default folder provides file synchronization | Read the folder through the authenticated API, create a file, request a scan and read the indexed file metadata | The active send-receive folder is `/home/syncthing/Sync` and the file is indexed with matching name and size | `tests/v19.sh` | +| Nginx provides system TLS and redirects ports 80 and 8384 to 443 | Validate Nginx, request HTTP and HTTPS entry paths, inspect the configured system certificate and test both port 8384 schemes | HTTPS serves Syncthing and both documented redirect paths reach port 443 | `tests/v19.sh` | +| Syncthing API access remains authenticated | Reject an anonymous REST request, extract the generated API key and query status and folder endpoints | Anonymous access is denied and key-authenticated API calls succeed | `tests/v19.sh` | +| Webmin and SSH provide documented administration | Request Webmin HTTPS and cite the unchanged Core layer | Webmin responds; Core 19 supplies inherited root and SSH behavior | `tests/v19.sh`; Core run `20260824t010251z-1634-32241`, source `24c82ee3540ce545422742b0e28ba6b687c53ec2`, verdict `PASS` | +| Syncthing updates are supervised through APT | Refresh metadata and inspect candidates without changing installed versions | Signed Trixie metadata is accepted and the third-party repository is absent | `tests/v19.sh` | + +## Updater check + +`tests/v19.sh` runs `apt-get update`, confirms eligible Trixie candidates for +`syncthing`, `nginx` and `python3-bcrypt`, and verifies the installed versions +remain unchanged. The documented supervised Syncthing update is: + +```sh +apt-get install --only-upgrade syncthing +systemctl restart syncthing@syncthing.service +``` + +## Known limitation + +The one-container acceptance validates the synchronization engine by creating +a file in the default send-receive folder, requesting a scan and reading its +indexed metadata through the authenticated API. It does not transfer that file +to a second Syncthing device because device pairing would add a second runtime +without exercising another appliance-specific integration boundary. + +Docker runtime validation does not exercise the installer, kernel, bootloader +or physical hardware. Syncthing adds no appliance-specific behavior at those +boundaries, so the validated Core 19 baseline supplies inherited evidence. + +## Deferred issues + +- Syncthing 1.29 may request a larger UDP receive buffer than the appliance's + inherited 2.5 MB setting on some kernels. Syncthing continues with the + available buffer and TCP remains available. The runtime acceptance checks + both TCP and UDP listeners; performance tuning is deferred unless normal + deployment evidence shows a material transfer impact. diff --git a/overlay/etc/apt/preferences.d/syncthing.pref b/overlay/etc/apt/preferences.d/syncthing.pref deleted file mode 100644 index 556bfef..0000000 --- a/overlay/etc/apt/preferences.d/syncthing.pref +++ /dev/null @@ -1,7 +0,0 @@ -Package: * -Pin: release apt.syncthing.net -Pin-Priority: 100 - -Package: syncthing -Pin: release apt.syncthing.net -Pin-Priority: 500 diff --git a/overlay/etc/apt/sources.list.d/syncthing.list b/overlay/etc/apt/sources.list.d/syncthing.list deleted file mode 100644 index cb44314..0000000 --- a/overlay/etc/apt/sources.list.d/syncthing.list +++ /dev/null @@ -1 +0,0 @@ -deb [signed-by=/usr/share/keyrings/syncthing.gpg] https://apt.syncthing.net/ syncthing stable diff --git a/overlay/etc/nginx/sites-available/syncthing b/overlay/etc/nginx/sites-available/syncthing index 855c8e0..cfe3e12 100644 --- a/overlay/etc/nginx/sites-available/syncthing +++ b/overlay/etc/nginx/sites-available/syncthing @@ -28,7 +28,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://localhost:8383; + proxy_pass http://127.0.0.1:8383; proxy_read_timeout 600s; proxy_send_timeout 600s; @@ -36,4 +36,3 @@ server { proxy_redirect off; } } - diff --git a/overlay/usr/lib/inithooks/firstboot.d/20regen-syncthing-secrets b/overlay/usr/lib/inithooks/firstboot.d/20regen-syncthing-secrets index 21801d5..24204cd 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/20regen-syncthing-secrets +++ b/overlay/usr/lib/inithooks/firstboot.d/20regen-syncthing-secrets @@ -8,7 +8,7 @@ systemctl enable syncthing@syncthing.service || true systemctl stop syncthing@syncthing.service || true # create backups, just in case... -FILES="config.xml cert.pem cert.key https-cert.pem https-key.pem csrftokens.txt" +FILES="config.xml cert.pem key.pem https-cert.pem https-key.pem csrftokens.txt" for file in ${FILES}; do file=${CONF_DIR}/${file} if [[ -f ${file} ]]; then diff --git a/plan/main b/plan/main index 9f4a622..c6d615d 100644 --- a/plan/main +++ b/plan/main @@ -1,5 +1,6 @@ #include python3-bcrypt +syncthing nginx /* nginx reverse proxy for Syncthing Web UI */ diff --git a/tests/v19.sh b/tests/v19.sh new file mode 100755 index 0000000..5fbecdf --- /dev/null +++ b/tests/v19.sh @@ -0,0 +1,189 @@ +#!/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} +config=/home/syncthing/.local/state/syncthing/config.xml +sync_root=/home/syncthing/Sync +response=/tmp/tkl-syncthing-response.$$ +headers=/tmp/tkl-syncthing-headers.$$ +cookie=/tmp/tkl-syncthing-cookie.$$ +login_json=/tmp/tkl-syncthing-login.$$ +folders_json=/tmp/tkl-syncthing-folders.$$ +file_json=/tmp/tkl-syncthing-file.$$ +policy=/tmp/tkl-syncthing-policy.$$ +probe_name=turnkey-v19-index-$$.txt +probe_path=$sync_root/$probe_name + +cleanup() { + rm -f -- "$response" "$headers" "$cookie" "$login_json" \ + "$folders_json" "$file_json" "$policy" "$probe_path" +} +trap cleanup EXIT + +systemctl --quiet is-active syncthing@syncthing.service nginx.service \ + multi-user.target +systemctl --quiet is-enabled syncthing@syncthing.service nginx.service +test "$(systemctl show --property=User --value \ + syncthing@syncthing.service)" = syncthing +nginx -t + +syncthing_package=$(dpkg-query -W -f='${Version}' syncthing) +nginx_package=$(dpkg-query -W -f='${Version}' nginx) +bcrypt_package=$(dpkg-query -W -f='${Version}' python3-bcrypt) +syncthing_version=$(syncthing --version) +grep -q '^syncthing v1\.29\.' <<<"$syncthing_version" +dpkg-query -S "$(readlink -f "$(command -v syncthing)")" >/dev/null + +test -s "$config" +test -s /home/syncthing/.local/state/syncthing/cert.pem +test -s /home/syncthing/.local/state/syncthing/key.pem +test "$(stat -c '%U:%G' "$config")" = syncthing:syncthing +grep -Eq '^[[:space:]]*
127\.0\.0\.1:8383
$' "$config" +grep -Eq '^[[:space:]]*syncthing$' "$config" +grep -Eq '^[[:space:]]*\$2' "$config" +grep -Eq '^[[:space:]]*true$' \ + "$config" +test -d "$sync_root" +test "$(stat -c '%U:%G' "$sync_root")" = syncthing:syncthing + +ss -ltn | grep -Eq '127\.0\.0\.1:8383[[:space:]]' +ss -ltn | grep -Eq ':22000[[:space:]]' +ss -lun | grep -Eq ':(21027|22000)[[:space:]]' +grep -Eq 'ssl_certificate[[:space:]]+/etc/ssl/private/cert.pem;' \ + /etc/nginx/snippets/ssl.conf + +curl --insecure --fail --silent --show-error --location \ + http://127.0.0.1/ >"$response" +grep -Fq 'ng-app="syncthing"' "$response" +curl --insecure --fail --silent --show-error \ + https://127.0.0.1/ >"$response" +grep -Fq 'ng-app="syncthing"' "$response" + +curl --silent --show-error --dump-header "$headers" --output /dev/null \ + http://127.0.0.1:8384/ +grep -q '^HTTP/.* 302' "$headers" +grep -Fqi 'Location: https://127.0.0.1/' "$headers" +curl --insecure --silent --show-error --dump-header "$headers" \ + --output /dev/null https://127.0.0.1:8384/ +grep -q '^HTTP/.* 302' "$headers" +grep -Fqi 'Location: https://127.0.0.1/' "$headers" + +unauth_status=$(curl --insecure --silent --output /dev/null \ + --write-out '%{http_code}' \ + https://127.0.0.1/rest/system/status) +test "$unauth_status" = 403 + +python3 - "$app_password" >"$login_json" <<'PYTHON' +import json +import sys + +json.dump({ + "username": "syncthing", + "password": sys.argv[1], + "stayLoggedIn": False, +}, sys.stdout) +PYTHON +login_status=$(curl --insecure --silent --show-error \ + --cookie-jar "$cookie" --output "$response" --write-out '%{http_code}' \ + --header 'Content-Type: application/json' \ + --data-binary @"$login_json" \ + https://127.0.0.1/rest/noauth/auth/password) +test "$login_status" = 204 +grep -q 'sessionid-' "$cookie" +curl --insecure --fail --silent --show-error --cookie "$cookie" \ + https://127.0.0.1/meta.js >"$response" +grep -q '"authenticated":true' "$response" + +api_key=$(python3 - "$config" <<'PYTHON' +import sys +import xml.etree.ElementTree as ET + +print(ET.parse(sys.argv[1]).getroot().findtext("gui/apikey")) +PYTHON +) +test -n "$api_key" +curl --insecure --fail --silent --show-error \ + --header "X-API-Key: $api_key" \ + https://127.0.0.1/rest/system/status >"$response" +python3 - "$response" <<'PYTHON' +import json +import sys + +status = json.load(open(sys.argv[1])) +assert status["myID"] +assert status["uptime"] > 0 +PYTHON + +curl --insecure --fail --silent --show-error \ + --header "X-API-Key: $api_key" \ + https://127.0.0.1/rest/config/folders >"$folders_json" +python3 - "$folders_json" <<'PYTHON' +import json +import sys + +folders = json.load(open(sys.argv[1])) +default = next(folder for folder in folders if folder["id"] == "default") +assert default["path"] == "/home/syncthing/Sync" +assert default["type"] == "sendreceive" +assert not default["paused"] +PYTHON + +printf 'syncthing-v19-index-ok\n' >"$probe_path" +chown syncthing:syncthing "$probe_path" +curl --insecure --fail --silent --show-error --request POST \ + --header "X-API-Key: $api_key" \ + 'https://127.0.0.1/rest/db/scan?folder=default' >/dev/null +indexed=false +for _ in {1..30}; do + if curl --insecure --fail --silent --show-error --get \ + --header "X-API-Key: $api_key" \ + --data-urlencode 'folder=default' \ + --data-urlencode "file=$probe_name" \ + https://127.0.0.1/rest/db/file >"$file_json" 2>/dev/null && + python3 - "$file_json" "$probe_name" <<'PYTHON' +import json +import sys + +entry = json.load(open(sys.argv[1]))["local"] +assert entry["name"] == sys.argv[2] +assert entry["size"] == len(b"syncthing-v19-index-ok\n") +assert not entry["deleted"] +PYTHON + then + indexed=true + break + fi + sleep 1 +done +test "$indexed" = true + +grep -Fxq 'Syncthing: https://$ipaddr:8384' \ + /etc/confconsole/services.txt +curl --insecure --fail --silent --show-error --head \ + https://127.0.0.1:12321/ >/dev/null + +before="$syncthing_package|$nginx_package|$bcrypt_package" +apt-get update >/dev/null +for package in syncthing nginx python3-bcrypt; 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}' syncthing)|$(dpkg-query -W -f='${Version}' nginx)|$(dpkg-query -W -f='${Version}' python3-bcrypt)" +test "$after" = "$before" +grep -Rqs '^Suites: trixie' /etc/apt/sources.list.d +! grep -RqiE 'bookworm|apt\.syncthing\.net' /etc/apt/sources.list.d + +cat >"$result" <