Skip to content

chore(etc): add comprehensive sandboxing#10421

Merged
calmh merged 12 commits intosyncthing:mainfrom
Valloric:main
Feb 11, 2026
Merged

chore(etc): add comprehensive sandboxing#10421
calmh merged 12 commits intosyncthing:mainfrom
Valloric:main

Conversation

@Valloric
Copy link
Copy Markdown
Contributor

@Valloric Valloric commented Oct 8, 2025

Update the existing minimal service hardening with a comprehensive sandbox to minimize blast damage from service compromise.

Please see the detailed code comments for an explanation of what is sandboxed.

Roughly, we limit: /dev, /proc, /sys, /tmp, AF_UNIX, AF_PACKET, uncommon syscalls plus anything io_uring1, kernel modules, clock, hostname, domain, cgroups, tons of kernel internals and more. We also enable a bunch of kernel namespaces for isolation.

In short: if there's a Systemd sandboxing option, it's enabled and tuned for syncthing's behavior (to the extent supported by my knowledge of syncthing).

Sadly, we cannot use ProtectSystem=strict by default because we don't know the directories that the user will be sharing. There's a big comment block explaining how users can enable it for "extra credit". :)

If the user did add the following options in a systemd drop-in file as the service file comments recommend:

  • ProtectSystem=strict
  • ReadWritePaths=/my/shared/dir1 /my/shared/dir2
  • ProtectHome=true
  • NoExecPaths=/
  • ExecPaths=/usr/bin/syncthing /usr/lib

Then the user would end up with a far more comprehensive sandbox than anything a container runtime (like Docker/Podman/whatever) would provide.

Much (but not all) of these options could be ported to the user/syncthing.service file, BUT it would require work. Systemd does not allow all of these options to be used with the user service manager, although using PrivateUsers=true would help with most of it.

I cannot justify the time investment to develop, audit and test the port to user/syncthing.service so I leave that for future PRs by interested contributors.

Tested on Debian Trixie (13) with the following versions:

  • v1.29.5, Linux (64-bit Intel/AMD)
  • latest HEAD (d3d3fc2 committed on Mon Oct 6 01:42:58 2025)

Documentation changes

None? This doesn't have to be user-visible, but it may make sense to tout the strong sandbox (and possible "extra credit" work users can do) if there's a desire for it. Up to maintainers.

Footnotes

  1. It's still a large source of exploits and syncthing doesn't use those syscalls at all. If either of the two changes, the syscall filter can be easily updated.

Update the existing minimal service hardening with a comprehensive
sandbox to minimize blast damage from service compromise.

Please see the detailed code comments for an explanation of what is
sandboxed.

Roughly, we limit: /dev, /proc, /tmp, AF_UNIX, AF_PACKET, execution of
_any_ binary other than "/usr/bin/syncthing" and "/usr/lib",
uncommon syscalls plus io_uring, tons of kernel internals and more. We
also enable a bunch of kernel namespaces for isolation.

In short, pretty much everything is sandboxed and specifically tuned for
syncthing's behavior.

Sadly, we cannot use ProtectSystem=strict by default because we don't
know the directories that the user will be sharing. There's a big
comment block explaining how users can enable it for "extra credit". :)

If the user did add the following options as the unit file recommends:

- ProtectSystem=strict
- ReadWritePaths=/my/shared/dir1 /my/shared/dir2
- ProtectHome=true

Then the user would end up with a *far* more comprehensive sandbox than
anything a container runtime (like Docker/Podman/whatever) would
provide.

Much (but not all) of these options could be ported to the
user/syncthing.service file, BUT it would require work. Systemd does not
allow all of these options to be used with the user service manager,
although using PrivateUsers=true would help with most of it.

I cannot justify the time investment to develop, audit and test the
port to user/syncthing.service so I leave that for interested
contributors.

Tested on Debian Trixie (13) with the following versions:
- v1.29.5, Linux (64-bit Intel/AMD)
- latest HEAD (d3d3fc2 committed on Mon Oct 6 01:42:58 2025)

Signed-off-by: Val Markovic <val@markovic.io>
@github-actions github-actions bot added the enhancement New features or improvements of some kind, as opposed to a problem (bug) label Oct 8, 2025
Copy link
Copy Markdown
Member

@calmh calmh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work everywhere Syncthing currently runs as a systemd service?

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Oct 8, 2025

Does this work everywhere Syncthing currently runs as a systemd service?

It should. Systemd only works on Linux and nothing in this PR is distro specific. It should work on any Linux distro that uses systemd.

It should even work on older versions of systemd that don't support some of these options; they will be ignored (with possibly a warning printed in the journal).

If systemd supports the option but the kernel does not (for whatever reason), systemd will ignore the option. Thus the sandbox is "best effort", as the systemd docs recommend it be used.

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Oct 8, 2025

Also, none of these options introduce external dependencies. These are all pure systemd+kernel features (ignored when not supported, as mentioned).

Syncthing supports calling user-provided binaries which can be in
arbitrary locations in the filesystem.

Signed-off-by: Val Markovic <val@markovic.io>
@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Oct 8, 2025

I pushed f48a680 which removes NoExecPaths from the default set of sandboxing options. It's now documented in the "extra credit" section.

Advice for maintainers: since this now makes it possible for an attacker to escalate access to the rest of the system by calling other binaries already present (LOTL), it would be prudent to use internal app sandboxing to limit which binaries syncthing is capable of executing.

For instance, it would be a great idea for syncthing code to sandbox the syncthing process so that it can only execute allow-listed binaries like syncthing, /usr/lib and the binaries the user provided in the config for the External File Versioning feature.

Setting up the necessary custom mount namespacing is a non-trivial amount of work though. But there are sandboxing libraries that can help with this. Tough sell, I know.

@ProactiveServices
Copy link
Copy Markdown
Contributor

I have already been using most of these for some time, but sadly never found the time to contribute something similar, so kudos. I've added in those I wasn't using and will feedback if I have any problems.

It should never exist, but if it does, syncthing running with a system
user would implicitly get access to /nonexistent on Debian-like systems
because it's the standard HOME folder for system users.

Signed-off-by: Val Markovic <val@markovic.io>
Signed-off-by: Val Markovic <val@markovic.io>
Files with setuid/setgid bits are designed to be used for privilege
escalation. Such files are *exceptionally* dangerous.

Signed-off-by: Val Markovic <val@markovic.io>
@Valloric
Copy link
Copy Markdown
Contributor Author

I've made a small tweak to prevent a systemd warning about io_uring_enter2 (code comments have details). I've also tightened the umask so it's 7027, which now also prevents creation of files with setuid/setgid bits which are designed for privilege escalation. Such files are incredibly dangerous.

@ProactiveServices Any further comments?
@calmh Any chance of this being merged in the foreseeable future? Or should I drop it?

@calmh
Copy link
Copy Markdown
Member

calmh commented Oct 25, 2025

I don't really use the systemd setup much and can't vouch for or maintain this, so I'd like someone else to step up and say they've tested it, that it won't cause any problems for the users already using it on upgrade, and that they'll make sure it works for future versions.

@bt90
Copy link
Copy Markdown
Contributor

bt90 commented Oct 25, 2025

I don't understand the hate for io_uring. Google is mostly having problems with older LTS kernels and many people are judging based on inflated CVE numbers. The kernel developers fixed the thread model in version 5.10, solving the most fundamental problems. I'm not saying that there won't be any future issues, but it's fine if you're not a cloud service provider 🤷

@Valloric
Copy link
Copy Markdown
Contributor Author

(Apologies for the delays, I'm down with COVID. I'll respond to comments when I've recovered.)

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Feb 2, 2026

@CoelacanthusHex I added a note about AF_NETLINK, thanks!
@marbens-arch Good point, I added AF_UNIX to allowed list! The web UI does not listen on a UNIX socket by default, but the docs explicitly state this is supported so it should continue to work.

I want to thank both of you for leaving comments because it pinged me and made me realize I had, uh, forgotten about this PR. 😅

@ProactiveServices I don't see the error message we both noticed (Supervising process 55825 which is not our child. We'll most likely not notice when it exits.) in my logs anymore:

Jan 29 12:05:39 vault systemd[1]: Starting syncthing@syncthing.service - Syncthing - Open Source Continuous File Synchronization for syncthing...
Jan 29 12:05:39 vault systemd[1]: Started syncthing@syncthing.service - Syncthing - Open Source Continuous File Synchronization for syncthing.
Jan 29 12:05:39 vault syncthing[2723]: [start] INFO: syncthing v1.29.5 "Gold Grasshopper" (go1.24.2 linux-amd64) debian@debian 2025-04-20 21:21:27 UTC
Jan 29 12:05:39 vault syncthing[2723]: [EYBZ4] INFO: My ID: <redacted>
Jan 29 12:05:39 vault syncthing[2723]: [EYBZ4] INFO: Hashing performance is 232.15 MB/s
Jan 29 12:05:39 vault syncthing[2723]: [EYBZ4] INFO: Overall send rate is unlimited, receive rate is unlimited

@calmh @ProactiveServices I'd be happy to mantain this service file going forward. I've been using syncthing for many years now and I have no intention to stop using it. I exclusively use the systemd service file on my servers. If there are any issues, an at-valloric ping on Github should reliably reach me.

Meanwhile, I've been using this sandboxed setup for several months now across several devices with zero issues. I think the PR is ready to merge.

@calmh

so I'd like someone else to step up and say they've tested it, that it won't cause any problems for the users already using it on upgrade, and that they'll make sure it works for future versions.

As mentioned, I've extensively tested this on my systems. This sandbox has been designed to be as widely applicable as possible. I of course cannot promise the absence of bugs or upgrading issues; people have all sorts of weird configurations. What I can commit to is fixing any reasonable1 problems people bring up after upgrading.

Footnotes

  1. "Reasonable" being defined as a change that would benefit the majority of syncthing's users and won't hurt the security of most users for the benefit of one person with a very odd setup.

Needed when binding the web interface to a Unix socket.

Signed-off-by: Val Markovic <val@markovic.io>
@marbens-arch marbens-arch changed the title feat(systemd) add comprehensive sandboxing feat(systemd): add comprehensive sandboxing Feb 2, 2026
# Example drop-in file location (assuming user "syncthing"):
# /etc/systemd/system/syncthing@syncthing.service.d/override.conf
#
## Elevated permissions to sync ownership (disabled by default),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these work and be enough with the stuff above in place? Some of it sure sounds like it would interfere with syncing ownership, like PrivateUsers.

Copy link
Copy Markdown
Contributor Author

@Valloric Valloric Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

The user would have to change PrivateUsers to false as well. I have re-examined all the other properties this PR is setting and I do not see anything else that would interfere.

UMask=7027 looks at first glance that it might be an issue, but that only sets more restricted file permissions at file creation time. It's meant to prevent accidentally wider-than-intended file access.

UMask= does not restrict chmod operations at all. I'm assuming syncthing downloads the files (creating them on disk) and then uses chmod/fchmod/fchmodat/etc syscalls to change the file permissions to something else (if the user is using that feature). UMask= would not interfere with that approach.

No other properties seem relevant.

I have updated the commented-out section to clarify this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing: RestrictSUIDSGID=true prevents setting the setuserid/setgroupid bits on any files. I'm assuming syncthing wouldn't sync that bit today as a security precaution, but if it did sync that bit today, this property would prevent it.

This would IMO be an excellent result because nobody in their right mind should be syncing files with SUID/SGID bits set. That's a security breach waiting to happen.

If syncthing would quietly sync this bit without this sandbox being enabled, I'd personally report that as a security issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Syncthing doesn't set suid/sgid bits.

However on the privateuser point, I think that means it needs to default to false or we'll break users who use ownership syncing and have applied the current recommended setup as an overlay.

Copy link
Copy Markdown
Contributor Author

@Valloric Valloric Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see your point.

I have removed PrivateUsers=true. I'd like to mention that PrivateUsers=true is the single strongest element of the sandbox this PR provides. Removing it meaningfully hurts the security of every user for the benefit of a handful that are using a non-standard setup.

If you ever change your mind, I'd be happy to put it back in.

Syncthing doesn't set suid/sgid bits.

(Thank you! ❤️ )

@GermanCoding
Copy link
Copy Markdown
Contributor

Just an FYI that this PR was my "this change broke my workflow" moment (https://xkcd.com/1172/). "Silently" changing the umask wasn't that awesome for me. I relied on having some files world-readable on a folder so that folder had "ignore permissions" enabled in order for syncthing to rely on the umask (instead of whatever permissions the other side uses), but with this new default you can't even stat the dir if you're not the owner. For folders where permissions are synced the umask doesn't matter anyway, but this is a rather significant change in permissions for all folders where ignore permissions is set.

(systemd override obviously fixed it, just a surprising behaviour change given that this was released as a minor patch)

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Mar 10, 2026

@GermanCoding I'm sorry this change affected your workflow, but a world-readable umask for a folder containing unknown PII is not a safe default. If you need it, you should set it explicitly in a systemd unit override (as you've discovered).

@agoeckner
Copy link
Copy Markdown
Contributor

agoeckner commented Mar 11, 2026

These changes broke the "inherit ownership" setting. To get it working again, I had to override the sandbox configuration to allow access to the chmod system call.

### Editing /etc/systemd/system/syncthing@admin.service.d/override.conf
### Anything between here and the comment below will become the contents of the drop-in file

[Service]
AmbientCapabilities=CAP_CHOWN CAP_FOWNER
CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER
SystemCallFilter=@system-service chmod

### Edits below this comment will be discarded

Thank you for the contribution though; this looks like a good change.

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Mar 11, 2026

@calmh Thoughts on the CAP_CHOWN/CAP_FOWNER above? These Linux capabilities give very broad powers to the syncthing process that could be easily used for privilege escalation attacks. CAP_CHOWN allows for "arbitrary changes to file UIDs and GIDs" and CAP_FOWNER is even broader.

I don't think a folder syncing service should have the access rights to change/ignore filesystem ownership across the entire OS. These rights cannot be scoped to a folder. I'm not sure what this "inherit ownership" setting is though, but IMO it cannot be worth this level of access.

WDYT? I'm wary of breaking existing functionality. Can we "solve" this with better documentation? Maybe something like "users wishing to use 'inherit ownership' on Linux will need to add the following properties to a systemd override file while keeping in mind the security exposure".

@calmh
Copy link
Copy Markdown
Member

calmh commented Mar 11, 2026

It requires what it requires. These capabilities weren't default earlier either and just need the proper docs to enable.

IMO it cannot be worth this level of access. WDYT?

I don't know what you're suggesting or implying here, but I suspect you need to chill. We're not going to remove features people are using just because you don't like the required capabilities.

@Valloric
Copy link
Copy Markdown
Contributor Author

@calmh I want to call out that a position of "zero breakage of shipped features" is not unreasonable. If that's the call syncthing maintainers wish to make, I'll carve out the necessary access.

We're not going to remove features people are using

This is fair.

just because you don't like the required capabilities.

This is unnecessary. :) It's not about personal distaste/opinions, it's about user security exposure. CAP_CHOWN/CAP_FOWNER provide significant opportunity for privilege escalation.

@Valloric
Copy link
Copy Markdown
Contributor Author

These capabilities weren't default earlier either and just need the proper docs to enable.

Now I'm confused... how did this PR then break the "inherit ownership" setting? @agoeckner Could you provide more info on the breakage you saw in an issue report? Please at-tag me in your issue.

@calmh
Copy link
Copy Markdown
Member

calmh commented Mar 11, 2026

Presumably because the previous additions to enable them are no longer enough given the additional sandboxing.

Or they were running the service as root.

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Mar 11, 2026

Presumably because the previous additions to enable them are no longer enough given the additional sandboxing.

Could you explain "previous additions" more? I'm not sure what you're referring too.

Or they were running the service as root.

This PR changes syncthing@.service, not syncthing.service. I guess it's possible @agoeckner used syncthing@root.service, but that would be a weird setup given that syncthing.service is right there... I'm assuming it's something else. We need more info from @agoeckner to track it down though.

@calmh
Copy link
Copy Markdown
Member

calmh commented Mar 11, 2026

@Valloric
Copy link
Copy Markdown
Contributor Author

This: https://docs.syncthing.net/users/autostart.html#permissions

That makes perfect sense! So this feature always required the user to add extra permissions.

We can fix this retroactively by adding:

AmbientCapabilities=
CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER
SystemCallFilter=@system-service chmod

in syncthing@.service. This results in no increase in permissions for people who aren't already setting AmbientCapabilities=CAP_CHOWN CAP_FOWNER. Setting CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER only sets a "cap" on maximum permissions that could ever be reached; adding capabilities here is not a huge deal (they're not granted, they just become possible to acquire in the future). Likewise, adding chmod to SystemCallFilter doesn't do much by itself because while syncthing can then invoke the syscall, it would just get a "permission denied" without AmbientCapabilities=CAP_CHOWN CAP_FOWNER.

I'll send a PR.

Valloric added a commit to Valloric/syncthing that referenced this pull request Mar 11, 2026
Syncthing docs in https://docs.syncthing.net/users/autostart.html#permissions
tell the user to set `AmbientCapabilities=CAP_CHOWN CAP_FOWNER` if the
user wishes to use the `syncOwnership` option.

syncthing#10421 broke `syncOwnership`
for users that followed that advice because the PR introduced
`CapabilityBoundingSet=` which cancels out any additional capabilities
granted with `AmbientCapabilities`.

(`AmbientCapabilities` _adds_ capabilities; `CapabilityBoundingSet`
_limits_ maximum capabilities to the specified set. Setting
`CapabilityBoundingSet` to an empty list prevents any capabilities from
being acquired in any way.)

This PR fixes the breakage by explicitly setting
  CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER

and also adding `chown` to `SystemCallFilter`. This does _not_ grant
any additional access rights to syncthing if the user is not explicitly
setting `AmbientCapabilities` as well, but it does loosen the sandbox
_a bit_. An attacker compromising the syncthing process could now more
easily expand their access to include CAP_CHOWN/CAP_FOWNER even if the
user is not setting `AmbientCapabilities`.
Valloric added a commit to Valloric/syncthing that referenced this pull request Mar 11, 2026
Syncthing docs in https://docs.syncthing.net/users/autostart.html#permissions
tell the user to set `AmbientCapabilities=CAP_CHOWN CAP_FOWNER` if the
user wishes to use the `syncOwnership` option.

syncthing#10421 broke `syncOwnership`
for users that followed that advice because the PR introduced
`CapabilityBoundingSet=` which cancels out any additional capabilities
granted with `AmbientCapabilities`.

(`AmbientCapabilities` _adds_ capabilities; `CapabilityBoundingSet`
_limits_ maximum capabilities to the specified set. Setting
`CapabilityBoundingSet` to an empty list prevents any capabilities from
being acquired in any way.)

This PR fixes the breakage by explicitly setting
  CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER

and also adding `chown` to `SystemCallFilter`. This does _not_ grant
any additional access rights to syncthing if the user is not explicitly
setting `AmbientCapabilities` as well, but it does loosen the sandbox
_a bit_. An attacker compromising the syncthing process could now more
easily expand their access to include CAP_CHOWN/CAP_FOWNER even if the
user is not setting `AmbientCapabilities`.

Signed-off-by: Val Markovic <val@markovic.io>
@Valloric
Copy link
Copy Markdown
Contributor Author

Sent #10602

Valloric added a commit to Valloric/syncthing that referenced this pull request Mar 11, 2026
Syncthing docs in https://docs.syncthing.net/users/autostart.html#permissions
tell the user to set `AmbientCapabilities=CAP_CHOWN CAP_FOWNER` if the
user wishes to use the `syncOwnership` option.

syncthing#10421 broke `syncOwnership`
for users that followed that advice because the PR introduced
`CapabilityBoundingSet=` which cancels out any additional capabilities
granted with `AmbientCapabilities`.

(`AmbientCapabilities` _adds_ capabilities; `CapabilityBoundingSet`
_limits_ maximum capabilities to the specified set. Setting
`CapabilityBoundingSet` to an empty list prevents any capabilities from
being acquired in any way.)

This PR fixes the breakage by explicitly setting

    CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER

This does _not_ grant any additional access rights to syncthing if the
user is not explicitly setting `AmbientCapabilities` as well, but it
does loosen the sandbox _a bit_. An attacker compromising the syncthing
process could now more easily expand their access to include
CAP_CHOWN/CAP_FOWNER even if the user is not setting
`AmbientCapabilities`.

Signed-off-by: Val Markovic <val@markovic.io>
calmh pushed a commit that referenced this pull request Mar 11, 2026
Syncthing docs in https://docs.syncthing.net/users/autostart.html#permissions
tell the user to set `AmbientCapabilities=CAP_CHOWN CAP_FOWNER` if the
user wishes to use the `syncOwnership` option.

#10421 broke `syncOwnership`
for users that followed that advice because the PR introduced
`CapabilityBoundingSet=` which cancels out any additional capabilities
granted with `AmbientCapabilities`.

(`AmbientCapabilities` _adds_ capabilities; `CapabilityBoundingSet`
_limits_ maximum capabilities to the specified set. Setting
`CapabilityBoundingSet` to an empty list prevents any capabilities from
being acquired in any way.)

This PR fixes the breakage by explicitly setting

    CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER

This does _not_ grant any additional access rights to syncthing if the
user is not explicitly setting `AmbientCapabilities` as well, but it
does loosen the sandbox _a bit_. An attacker compromising the syncthing
process could now more easily expand their access to include
CAP_CHOWN/CAP_FOWNER even if the user is not setting
`AmbientCapabilities`.

Signed-off-by: Val Markovic <val@markovic.io>
@agoeckner
Copy link
Copy Markdown
Contributor

agoeckner commented Mar 11, 2026

Hey @Valloric! Sorry, I'm a little bit confused. Are you by chance using an LLM for your responses and PRs? Edit: Sorry, that was pretty rude of me. I totally misunderstood you!

The PR #10602 doesn't actually address what I reported, and it contradicts your previous work, where you had already included the AmbientCapabilities and CapabilityBoundingSet as commented options that the user could enable.

The minimal change required is to add chmod to SystemCallFilter, which allows Syncthing's ownership inheritance feature to work.

I suppose that you could also set the CapabilityBoundingSet to CAP_CHOWN CAP_FOWNER. The user would then only need to set AmbientCapabilities=CAP_CHOWN CAP_FOWNER.

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Mar 11, 2026

Hey @Valloric! Sorry, I'm a little bit confused. Are you by chance using an LLM for your responses and PRs?

No, I wrote every word in every comment and PR. I despise LLM-speak. Please don't "jump the gun" and accuse people of LLM-speak with zero evidence.

The PR #10602 doesn't actually address what I reported, and it contradicts your previous work, where you had already included the AmbientCapabilities and CapabilityBoundingSet as commented options that the user could enable.

Your report contained very little information. Please provide more information in the future so we don't have to reverse-engineer the issue we think you might have encountered. Consider filing an issue report instead of leaving comments on closed PRs.

I suppose that you could also set the CapabilityBoundingSet to CAP_CHOWN CAP_FOWNER. The user would then only need to set AmbientCapabilities=CAP_CHOWN CAP_FOWNER.

Yes, this is what @calmh and I were guessing were the issue you encountered. CapabilityBoundingSet would have broken any override that sets just AmbientCapabilities, as the docs stated.

The minimal change required is to add chmod to SystemCallFilter, which allows Syncthing's ownership inheritance feature to work.

As the discussion in #10602 pointed out, chmod is already included in @system-service1 so adding it to SystemCallFilter does absolutely nothing (perhaps it was an LLM hallucination?).2 The linked source is direct from systemd.

Did you try out the fix in #10602? Please confirm if the problem persist after those changes are applied.

Footnotes

  1. @system-service includes @file-system, which in turn includes chmod. @system-service also includes @chown.

  2. Also, chmod changes file/dir permission bits, not ownership; chown changes that. So chmod shouldn't matter at all for the syncOwnership feature. Still, both chown and chmod (and related syscalls) are all included in @system-service.

@agoeckner
Copy link
Copy Markdown
Contributor

agoeckner commented Mar 11, 2026

Sorry, I'm just so used to people putting everything through an LLM. It's a perpetual peeve of mine!

Initially, I only used the override suggested in the systemd unit comments/documentation:

AmbientCapabilities=CAP_CHOWN CAP_FOWNER
CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER

However, the inherit ownership feature did not work until I added the SystemCallFilter override as well:

AmbientCapabilities=CAP_CHOWN CAP_FOWNER
CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER
SystemCallFilter=@system-service chmod

I'm not sure what's going on here. You're right that @system-service should already include it.

I wonder if it's being removed by this line: https://github.com/syncthing/syncthing/pull/10421/changes#diff-9da7d7beb75aa041e0bd9698a0b479b0e769baa8ea50f816dca52f77fd16096fR124
My override would actually have overridden/removed that line too.

edit: Yep, that's the case. chmod is removed by the ~@privileged: https://github.com/systemd/systemd/blob/44b050fe761c579f5b9f7392aacc697ed1311b35/src/shared/seccomp-util.c#L778

@Valloric
Copy link
Copy Markdown
Contributor Author

Valloric commented Mar 11, 2026

edit: Yep, that's the case. chmod is removed by the ~@PRIVILEGED: https://github.com/systemd/systemd/blob/44b050fe761c579f5b9f7392aacc697ed1311b35/src/shared/seccomp-util.c#L778

You might be right! Very surprising behavior from systemd here. Docs:

SystemCallFilter=
           Takes a space-separated list of system call names or system call groups. If this setting
           is used, system calls executed by the unit processes except for the listed ones will
           result in the system call being denied (allow-listing). If the first character of the
           list is "~", the effect is inverted: only the listed system calls will be denied
           (deny-listing). This option may be specified more than once, in which case the filter
           masks are merged. If the empty string is assigned, the filter is reset, all prior
           assignments will have no effect.

So what does "merged" mean then? Does the order of SystemCallFilter declaration matter? What would happen if we listed SystemCallFilter=@system-service second, after SystemCallFilter=~@privileged ...? Should we merge declarations into one SystemCallFilter=, with careful ordering or items in the list?

I'll need to read some systemd source code and experiment, the docs are definitely not clear on this.

@agoeckner Could you file an issue to track this? 😃

@imsodin
Copy link
Copy Markdown
Member

imsodin commented Mar 11, 2026

Besides the @privileged issue, there's also a mixup of chmod and chown (not disagreeing that it's confusing overall :) ):

Confirmed by looking at the systemd source code: https://github.com/systemd/systemd/blob/44b050fe761c579f5b9f7392aacc697ed1311b35/src/shared/seccomp-util.c#L951

@System-service includes @chown and fchmod (plus variants). I'll remove this.

From here:
https://app.datadoghq.com/monitors/262149163?group=rabbitmq_queue%3Adbuploadersensdb1aws%2Crabbitmq_vhost%3Aunified_stream&from_ts=1773261643000&to_ts=1773262843000&event_id=8539598550854497891&link_source=monitor_notif&link_monitor_id=262149163&link_event_id=8539598550854497891

Comment says it includes fchmod et al, but the linked code only showns chown - chmod is part of @file-system not @system-service. So that might be why we still need it explicitly there - no idea how that interacts with @privileged.

@agoeckner
Copy link
Copy Markdown
Contributor

agoeckner commented Mar 11, 2026

Very true, but @file-system appears to be part of @system-service.

And sure @Valloric let me create one.

Regarding chmod vs chown... Let me double-check which one I actually added to my server's configuration once I get home. Perhaps I made a mistake when reporting my solution :)

@agoeckner
Copy link
Copy Markdown
Contributor

agoeckner commented Mar 12, 2026

New issue created: #10603.

I can also confirm that I did include chmod instead of the correct chown in my filter settings. However, I think what's actually important is that my filter override removed ~@privileged.

Valloric added a commit to Valloric/syncthing that referenced this pull request Mar 12, 2026
IFF the user enables the `syncOwnership` feature AND sets
`AmbientCapabilities=CAP_CHOWN CAP_FOWNER` as the docs in
https://docs.syncthing.net/users/autostart.html#permissions state,
THEN syncthing needs to use the `chown` syscall.

PR syncthing#10421 added a comprehensive sandbox that breaks `syncOwnership`.
In PR syncthing#10602 we fixed one part, which is expanding the default
`CapabilityBoundingSet` (see the PR for details).

But there's a very subtle bug that this PR fixes. PR syncthing#10421 sets the
following properties:

    SystemCallFilter=@System-service
    SystemCallFilter=~@PRIVILEGED io_uring_enter io_uring_register io_uring_setup

(Systemd merges `SystemCallFilter` values; we had to set the property
twice because to negate syscalls, the whole list has to start with `~`.)

The goal was to allow all syscalls in the `@system-service` set, BUT
disallow any `@privileged` syscalls and the `io_uring*` syscalls.

But the sets are not disjoint; `chown` is in both `@system-service` and
in `@privileged`, so it is removed from the allow list by the second
property value.

This property is also parsed in a very peculiar way. From systemd docs:

> If you specify both types of this option (i.e. allow-listing and
> deny-listing), the first encountered will take precedence and will
> dictate the default action (termination or approval of a system call).
> Then the next occurrences of this option will add or delete the listed
> system calls from the set of the filtered system calls, depending of its
> type and the default action. (For example, if you have started with an
> allow list rule for read() and write(), and right after it add a deny
> list rule for write(), then write() will be removed from the set.)

Not only does the order of `SystemCallFilter` properties matter (later
ones can undo effects of prior ones), but the _type_ of the _first_
property sets the overall behavior of the syscall filter: if the first
`SystemCallFilter` value is an allow list, then all syscalls that are
not specified are disallowed by default (and reverse if the first value
is a deny list).

Of course, this is completely different from how other allow/deny lists
are implemented in systemd; for example, `IPAddress[Allow|Deny]`
properties don't work like this at all. >:(

Since this complexity has bit us once, we're removing the additional
deny list of syscalls and sticking with just
`SystemCallFilter=@system-service`.

This leaves some privileged syscalls in the allow list. Other options
would require entering the "deny list by default" mode and deny lists
are less secure than allow lists in general because they have to be
maintained (the kernel always adds new syscalls). The rest of the
sandbox (capability bounds) should be sufficient.

Fixes syncthing#10603

Signed-off-by: Val Markovic <val@markovic.io>
calmh pushed a commit that referenced this pull request Mar 13, 2026
fix(systemd): Add back chown allowed syscalls

IFF the user enables the `syncOwnership` feature AND sets
`AmbientCapabilities=CAP_CHOWN CAP_FOWNER` as the docs in
https://docs.syncthing.net/users/autostart.html#permissions state,
THEN syncthing needs to use the `chown` syscall.

PR #10421 added a comprehensive sandbox that breaks `syncOwnership`.
In PR #10602 we fixed one part, which is expanding the default
`CapabilityBoundingSet` (see the PR for details).

But there's a very subtle bug that this PR fixes. PR #10421 sets the
following properties:

    SystemCallFilter=@System-service
    SystemCallFilter=~@PRIVILEGED io_uring_enter io_uring_register io_uring_setup

(Systemd merges `SystemCallFilter` values; we had to set the property
twice because to negate syscalls, the whole list has to start with `~`.)

The goal was to allow all syscalls in the `@system-service` set, BUT
disallow any `@privileged` syscalls and the `io_uring*` syscalls.

But the sets are not disjoint; `chown` is in both `@system-service` and
in `@privileged`, so it is removed from the allow list by the second
property value.

This property is also parsed in a very peculiar way. From systemd docs:

> If you specify both types of this option (i.e. allow-listing and
> deny-listing), the first encountered will take precedence and will
> dictate the default action (termination or approval of a system call).
> Then the next occurrences of this option will add or delete the listed
> system calls from the set of the filtered system calls, depending of its
> type and the default action. (For example, if you have started with an
> allow list rule for read() and write(), and right after it add a deny
> list rule for write(), then write() will be removed from the set.)

Not only does the order of `SystemCallFilter` properties matter (later
ones can undo effects of prior ones), but the _type_ of the _first_
property sets the overall behavior of the syscall filter: if the first
`SystemCallFilter` value is an allow list, then all syscalls that are
not specified are disallowed by default (and reverse if the first value
is a deny list).

Of course, this is completely different from how other allow/deny lists
are implemented in systemd; for example, `IPAddress[Allow|Deny]`
properties don't work like this at all. >:(

Since this complexity has bit us once, we're removing the additional
deny list of syscalls and sticking with just
`SystemCallFilter=@system-service`.

This leaves some privileged syscalls in the allow list. Other options
would require entering the "deny list by default" mode and deny lists
are less secure than allow lists in general because they have to be
maintained (the kernel always adds new syscalls). The rest of the
sandbox (capability bounds) should be sufficient.

Fixes #10603

Signed-off-by: Val Markovic <val@markovic.io>
calmh added a commit to calmh/syncthing that referenced this pull request Mar 21, 2026
* main: (45 commits)
  chore(gui, man, authors): update docs, translations, and contributors
  chore(sqlite): reduce max open connections, keep them open permanently (fixes syncthing#10592) (syncthing#10596)
  fix(systemd): add back chown allowed syscalls (syncthing#10605)
  fix(systemd): support overrides for syncOwnership (syncthing#10602)
  chore(gui, man, authors): update docs, translations, and contributors
  fix(protocol): verify compressed message length before decompression (syncthing#10595)
  build(deps): update dependencies (syncthing#10588)
  chore: trigger rebuild
  chore(gui, man, authors): update docs, translations, and contributors
  chore(gui, man, authors): update docs, translations, and contributors
  chore(gui, man, authors): update docs, translations, and contributors
  chore(config, connections): use same reconnection interval for QUIC and TCP (fixes syncthing#10507) (syncthing#10573)
  chore: build with Go 1.26; use Go 1.25 features (syncthing#10570)
  chore(etc): add more comprehensive systemd sandboxing (syncthing#10421)
  fix(gui): remove width limit for language select items (syncthing#10531)
  fix(gui): show restarting modal during upgrade restart (fixes syncthing#1248) (syncthing#10566)
  chore(db): add ability to wait for programmatically started database maintenance, query last maintenance time (syncthing#10565)
  chore(gui, man, authors): update docs, translations, and contributors
  chore(gui): add id and name to Stay logged in checkbox for password managers (syncthing#10558)
  refactor: remove unused support for Azure blob stores
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants