Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)*
Expand All @@ -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
14 changes: 14 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -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 <release@turnkeylinux.org> 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.
Expand Down
5 changes: 0 additions & 5 deletions conf.d/main
Original file line number Diff line number Diff line change
@@ -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
Expand Down
82 changes: 82 additions & 0 deletions docs/v19.0-testing.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 0 additions & 7 deletions overlay/etc/apt/preferences.d/syncthing.pref

This file was deleted.

1 change: 0 additions & 1 deletion overlay/etc/apt/sources.list.d/syncthing.list

This file was deleted.

3 changes: 1 addition & 2 deletions overlay/etc/nginx/sites-available/syncthing
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ 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;

proxy_redirect off;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions plan/main
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <turnkey/base>

python3-bcrypt
syncthing

nginx /* nginx reverse proxy for Syncthing Web UI */
189 changes: 189 additions & 0 deletions tests/v19.sh
Original file line number Diff line number Diff line change
@@ -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:]]*<address>127\.0\.0\.1:8383</address>$' "$config"
grep -Eq '^[[:space:]]*<user>syncthing</user>$' "$config"
grep -Eq '^[[:space:]]*<password>\$2' "$config"
grep -Eq '^[[:space:]]*<insecureSkipHostcheck>true</insecureSkipHostcheck>$' \
"$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" <<EOF
package_source=Debian 13 Trixie APT repositories for Syncthing, Nginx and Python bcrypt
installed_version=syncthing $syncthing_package ($syncthing_version); nginx $nginx_package; python3-bcrypt $bcrypt_package
runtime_checks=normal init; Syncthing and Nginx service supervision; syncthing service identity; localhost GUI binding; HTTP, HTTPS and port 8384 proxy paths; authenticated GUI session; authenticated REST API; default send-receive folder; practical file creation, scan and indexed readback; Webmin management endpoint
updater_command=apt-get update; apt-cache policy syncthing nginx python3-bcrypt; apt-get install --only-upgrade syncthing
updater_result=signed Trixie metadata refreshed; eligible package candidates found; installed versions unchanged
updater_channel=Debian and TurnKey Trixie APT repositories
integrity_evidence=APT accepted signed repository metadata through configured keyrings; the third-party Syncthing repository and Bookworm sources are absent
EOF