catalog / docker.io/syncthing/syncthing

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

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/syncthing/syncthing@sha256:4a961394ca47…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/syncthing.sh
workload sha256979c098dea86…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
CHOWNrequiredcontainer failed to start: 2026-07-16 03:59:19 ERR Failed to ensure directory exists (error=mkdir /var/syncthi
DAC_OVERRIDErequiredconfig.xml never appeared: 2026-07-16 04:00:21 WRN Failed to acquire open port (mapping=[::]:22000/TCP id=NAT-
FSETIDremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
FOWNERremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
MKNODremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
NET_RAWremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
SETGIDrequiredcontainer exited: su-exec: setgroups(1000): Operation not permitted
SETUIDrequiredcontainer failed to start: su-exec: setuid(1000): Operation not permitted
SETFCAPremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
SETPCAPremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
NET_BIND_SERVICEremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
SYS_CHROOTremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
KILLremovablecorrect: REST status + folder add/readback ok, syncthing uid=1000 (non-root)
AUDIT_WRITEremovablecorrect: 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_optno-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

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/syncthing/syncthing@sha256:4a961394ca47…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/syncthing.sh
workload sha256979c098dea86…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
/tmpremovablecorrect: 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

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)"}