Running Syncthing as a System User on NixOS

Syncthing on NixOS runs as a system service by default and provides an option to run the service as your user. To run syncthing as the system user named syncthing, and also share folders in your user’s home directory, there are a few permissions we’ll need to set.

The file access capabilities we need are as follows:

To satisfy the above requirements, we can set the following to ensure correct permissions:

The first four are set as follows:

let
  user = "alan";
in
{
  users.users.syncthing.extraGroups = [ "users" ];
  users.users."${user}".extraGroups = [ "syncthing" ];
  systemd.services.syncthing.serviceConfig.UMask = "0007";
  systemd.tmpfiles.rules = [
    "d /home/${user} 0750 alan syncthing"
  ];
}

For the setgid flag permissions (2770), let’s set the permissions for all of our shared folders:

let
  user = "alan";
  folders = {
    "text" = {
      id = "text";
      path = "/home/${user}/text";
      ignorePerms = true;
    };
    "documents" = {
      id = "documents";
      path = "/home/${user}/documents";
      ignorePerms = true;
    };
  };

  inherit (builtins) map attrValues;
in
{
  systemd.tmpfiles.rules = (map (folder: "d ${folder.path} 2770 ${user} syncthing") (attrValues folders));
}

A Remaining Edgecase

The only permissions issue I’ve observed is that moving directories and files into the shared folder won’t trigger the setgid flag. The following find commands will fix the group permissions:

find "$FOLDER" -type f \( ! -group syncthing -or ! -perm -g=rw \) -not -path "*/.st*" -exec chgrp syncthing {} \; -exec chmod g+rw {} \;
find "$FOLDER" -type d \( ! -group syncthing -or ! -perm -g=rwxs \) -not -path "*/.st*" -exec chgrp syncthing {} \; -exec chmod g+rwxs {} \;

To automate this, two solutions are:

A timed job is simpler, so let’s go with that. Using the tasks module I previously wrote about, we can run our find command that fixes permissions for each syncthing directory.

tasks.fix-syncthing-permissions = {
  user = "alan";
  onCalendar = "*-*-* 18:00:00";
  script = let
    folders = pkgs.lib.concatMapStringsSep " " (folder: folder.path) (builtins.attrValues config.services.syncthing.folders);
    in ''
    for FOLDER in ${folders}; do
      find "$FOLDER" -type f \( ! -group syncthing -or ! -perm -g=rw \) -not -path "*/.st*" -exec chgrp syncthing {} \; -exec chmod g+rw {} \;
      find "$FOLDER" -type d \( ! -group syncthing -or ! -perm -g=rwxs \) -not -path "*/.st*" -exec chgrp syncthing {} \; -exec chmod g+rwxs {} \;
    done
  '';
};

Remarks

I love how simple it is to template my operating system configuration with NixOS. If I add a new syncthing folder, an appropriate tmpfiles rule will be set for it and the timed job to fix permissions will run for it as well. I don’t need to remember where the files are on my system, or even which files need to be changed. And if I do need to change something, it’s all specified in the code.

Future Work

All of these permissions shenanigans may be avoided entirely by using a bindfs fuse mount that remaps permissions:

bindfs --map=237/1000,@237/@100 /var/lib/syncthing/folder /home/alan/folder

It isn’t clear to me if this is supported by the `filesystems` option.