docker.io/syncthing/syncthing
validated pin current tags: 2.0
syncthing — minimum Linux capabilities, derived by drop-test against the default invocation. Trims the full Docker default cap set 14 -> 4. The entrypoint starts as root, ensures/chowns the /var/syncthing state dir (CHOWN + DAC_OVERRIDE), and su-exec drops to the default PUID 1000 (SETGID/SETUID). All startup caps; the GUI/API (:8384) and sync protocol (:22000) are unprivileged (no NET_BIND_SERVICE). A deployment running user: against a pre-owned state dir drops to []. See criteria/docker.io/syncthing/syncthing.md. filesystem: read_only:true, tmpfs:[]. syncthing's state (config, keys, index DB) lives in /var/syncthing, a declared VOLUME (auto-mounted writable; a named volume in production, never tmpfs). Under a read-only rootfs it starts and serves its API with no tmpfs. /tmp was drop-tested and is NOT required. Derived by drop-test.
Use it
services:
syncthing:
image: docker.io/syncthing/syncthing:2.0
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, SETGID, SETUID]
security_opt: ["no-new-privileges:true"]
read_only: true
This profile has multiple dimensions and is applied as a unit. Dimensions can interact — a capability can be required only because of a sibling read-only/tmpfs recommendation — so where they do, the minimum was derived under the sibling dimension's context. See the criteria doc and each dimension's recorded invocation below before applying them separately.
Dimension: capabilities
cap_drop: ALL + cap_add: CHOWN, DAC_OVERRIDE, SETGID, SETUID
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/syncthing/syncthing@sha256:4a961394ca47… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/syncthing.sh |
| workload sha256 | 979c098dea86… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
CHOWN | required | container failed to start: 2026-07-16 03:59:19 ERR Failed to ensure directory exists (error=mkdir /var/syncthi |
DAC_OVERRIDE | required | config.xml never appeared: 2026-07-16 04:00:21 WRN Failed to acquire open port (mapping=[::]:22000/TCP id=NAT- |
FSETID | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
FOWNER | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
MKNOD | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
NET_RAW | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
SETGID | required | container exited: su-exec: setgroups(1000): Operation not permitted |
SETUID | required | container failed to start: su-exec: setuid(1000): Operation not permitted |
SETFCAP | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
SETPCAP | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
NET_BIND_SERVICE | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
SYS_CHROOT | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
KILL | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
AUDIT_WRITE | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
Recorded invocation (run_config)
The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.
| security_opt | no-new-privileges:true |
|---|
Workload script (profiles/workloads/syncthing.sh)
#!/usr/bin/env bash
# Workload exerciser for the syncthing reference image. Reads the generated
# API key from config.xml (as the syncthing uid), asserts system/status
# returns a device ID, adds a folder via the config REST API and reads it
# back. In-image curl; capture-then-match.
# Required env: SYNCTHINGCONTAINER (target container name or id).
set -euo pipefail
: "${SYNCTHINGCONTAINER:?SYNCTHINGCONTAINER must be set}"
C="${SYNCTHINGCONTAINER}"
deadline=$((SECONDS + 60))
until docker exec "$C" test -s /var/syncthing/config/config.xml 2>/dev/null; do
(( SECONDS >= deadline )) && { echo "config.xml never appeared" >&2; exit 1; }
sleep 2
done
KEY="$(docker exec "$C" sh -c 'grep -oE "<apikey>[^<]*" /var/syncthing/config/config.xml | cut -d">" -f2')"
[ -n "$KEY" ] || { echo "no API key" >&2; exit 1; }
deadline=$((SECONDS + 45))
while :; do
st="$(docker exec --user 1000 "$C" curl -s --max-time 5 -H "X-API-Key: $KEY" http://localhost:8384/rest/system/status 2>/dev/null)"
case "$st" in *'"myID"'*) break;; esac
(( SECONDS >= deadline )) && { echo "system/status never answered" >&2; exit 1; }
sleep 2
done
code="$(docker exec --user 1000 "$C" curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X PUT -H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d '{"id":"csd","label":"csd","path":"/var/syncthing/csd-folder"}' http://localhost:8384/rest/config/folders/csd 2>/dev/null)"
case "$code" in 2??) ;; *) echo "folder add failed: HTTP ${code:-none}" >&2; exit 1;; esac
back="$(docker exec --user 1000 "$C" curl -s --max-time 5 -H "X-API-Key: $KEY" http://localhost:8384/rest/config/folders/csd 2>/dev/null)"
case "$back" in *csd*) ;; *) echo "folder readback failed" >&2; exit 1;; esac
Dimension: filesystem
read_only: true, no tmpfs
| tool | container-sec-derive 0.7.0 |
|---|---|
| observer | drop-test |
| validated image | docker.io/syncthing/syncthing@sha256:4a961394ca47… |
| validated date | 2026-07-16 |
| confidence | high |
| validated via | drop-test, ci-smoke |
| workload | profiles/workloads/syncthing.sh |
| workload sha256 | 979c098dea86… |
| ig version | v0.51.0 |
Drop-test evidence
Each candidate removed in turn, the container restarted, and the workload re-verified.
| Removed | Verdict | Observed |
|---|---|---|
/tmp | removable | correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root) |
Workload script (profiles/workloads/syncthing.sh)
#!/usr/bin/env bash
# Workload exerciser for the syncthing reference image. Reads the generated
# API key from config.xml (as the syncthing uid), asserts system/status
# returns a device ID, adds a folder via the config REST API and reads it
# back. In-image curl; capture-then-match.
# Required env: SYNCTHINGCONTAINER (target container name or id).
set -euo pipefail
: "${SYNCTHINGCONTAINER:?SYNCTHINGCONTAINER must be set}"
C="${SYNCTHINGCONTAINER}"
deadline=$((SECONDS + 60))
until docker exec "$C" test -s /var/syncthing/config/config.xml 2>/dev/null; do
(( SECONDS >= deadline )) && { echo "config.xml never appeared" >&2; exit 1; }
sleep 2
done
KEY="$(docker exec "$C" sh -c 'grep -oE "<apikey>[^<]*" /var/syncthing/config/config.xml | cut -d">" -f2')"
[ -n "$KEY" ] || { echo "no API key" >&2; exit 1; }
deadline=$((SECONDS + 45))
while :; do
st="$(docker exec --user 1000 "$C" curl -s --max-time 5 -H "X-API-Key: $KEY" http://localhost:8384/rest/system/status 2>/dev/null)"
case "$st" in *'"myID"'*) break;; esac
(( SECONDS >= deadline )) && { echo "system/status never answered" >&2; exit 1; }
sleep 2
done
code="$(docker exec --user 1000 "$C" curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X PUT -H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d '{"id":"csd","label":"csd","path":"/var/syncthing/csd-folder"}' http://localhost:8384/rest/config/folders/csd 2>/dev/null)"
case "$code" in 2??) ;; *) echo "folder add failed: HTTP ${code:-none}" >&2; exit 1;; esac
back="$(docker exec --user 1000 "$C" curl -s --max-time 5 -H "X-API-Key: $KEY" http://localhost:8384/rest/config/folders/csd 2>/dev/null)"
case "$back" in *csd*) ;; *) echo "folder readback failed" >&2; exit 1;; esac
Evidence & provenance
- Validation criteria — per-image scenarios and pass criteria
- Profile source in the repository
- Version history — every previously published pin, with the full profile as validated against it
Raw profile YAML
# syncthing — minimum Linux capabilities, derived by drop-test against the
# default invocation. Trims the full Docker default cap set 14 -> 4. The
# entrypoint starts as root, ensures/chowns the /var/syncthing state dir
# (CHOWN + DAC_OVERRIDE), and su-exec drops to the default PUID 1000
# (SETGID/SETUID). All startup caps; the GUI/API (:8384) and sync protocol
# (:22000) are unprivileged (no NET_BIND_SERVICE). A deployment running user:
# against a pre-owned state dir drops to []. See
# criteria/docker.io/syncthing/syncthing.md.
# filesystem: read_only:true, tmpfs:[]. syncthing's state (config, keys, index
# DB) lives in /var/syncthing, a declared VOLUME (auto-mounted writable; a named
# volume in production, never tmpfs). Under a read-only rootfs it starts and
# serves its API with no tmpfs. /tmp was drop-tested and is NOT required.
# Derived by drop-test.
#
schema_version: "1.5"
image: docker.io/syncthing/syncthing
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/syncthing/syncthing.html
applies_to:
tags: ["2.0"]
status: validated
dimensions:
capabilities:
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, SETGID, SETUID]
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/syncthing/syncthing@sha256:4a961394ca471f4e48f31ad2cef50697e502c8799e2e98477a1c3844e0c5bc54
validated_date: "2026-07-16"
duration_seconds: 240
confidence: high
workload: profiles/workloads/syncthing.sh
workload_sha256: "979c098dea86a8a4fe4b87c1e5c3b80ddff17f45290c6a54912769a8e05215ad"
validated_via: [drop-test, ci-smoke]
observation_backend:
ig_version: v0.51.0
gadgets: []
run_config:
user: ""
command: []
entrypoint: ""
network: ""
pid: ""
devices: []
security_opt: ["no-new-privileges:true"]
mounts: []
env: []
drop_test:
checks:
- {removed: CHOWN, required: true, observed: "container failed to start: 2026-07-16 03:59:19 ERR Failed to ensure directory exists (error=mkdir /var/syncthi"}
- {removed: DAC_OVERRIDE, required: true, observed: "config.xml never appeared: 2026-07-16 04:00:21 WRN Failed to acquire open port (mapping=[::]:22000/TCP id=NAT-"}
- {removed: FSETID, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: FOWNER, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: MKNOD, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: NET_RAW, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: SETGID, required: true, observed: "container exited: su-exec: setgroups(1000): Operation not permitted"}
- {removed: SETUID, required: true, observed: "container failed to start: su-exec: setuid(1000): Operation not permitted"}
- {removed: SETFCAP, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: SETPCAP, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: NET_BIND_SERVICE, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: SYS_CHROOT, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: KILL, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
- {removed: AUDIT_WRITE, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}
filesystem:
read_only: true
tmpfs: []
derivation:
tool: container-sec-derive
tool_version: "0.7.0"
sidecar_schema_version: "1.5"
observer: drop-test
validated_image: docker.io/syncthing/syncthing@sha256:4a961394ca471f4e48f31ad2cef50697e502c8799e2e98477a1c3844e0c5bc54
validated_date: "2026-07-16"
duration_seconds: 2
confidence: high
workload: profiles/workloads/syncthing.sh
workload_sha256: "979c098dea86a8a4fe4b87c1e5c3b80ddff17f45290c6a54912769a8e05215ad"
validated_via: [drop-test, ci-smoke]
observation_backend:
ig_version: v0.51.0
gadgets: []
run_config:
user: ""
command: []
entrypoint: ""
network: ""
pid: ""
devices: []
security_opt: []
mounts: []
env: []
drop_test:
checks:
- {removed: /tmp, required: false, observed: "correct: REST status + folder add/readback ok, syncthing uid=1000 (non-root)"}