diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6d05e6b7b09..a25e28775e4 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1230,6 +1230,7 @@ ./services/web-apps/powerdns-admin.nix ./services/web-apps/prosody-filer.nix ./services/web-apps/restya-board.nix + ./services/web-apps/sftpgo.nix ./services/web-apps/rss-bridge.nix ./services/web-apps/selfoss.nix ./services/web-apps/shiori.nix diff --git a/nixos/modules/services/web-apps/sftpgo.nix b/nixos/modules/services/web-apps/sftpgo.nix new file mode 100644 index 00000000000..846478ecbd6 --- /dev/null +++ b/nixos/modules/services/web-apps/sftpgo.nix @@ -0,0 +1,375 @@ +{ options, config, lib, pkgs, utils, ... }: + +with lib; + +let + cfg = config.services.sftpgo; + defaultUser = "sftpgo"; + settingsFormat = pkgs.formats.json {}; + configFile = settingsFormat.generate "sftpgo.json" cfg.settings; + hasPrivilegedPorts = any (port: port > 0 && port < 1024) ( + catAttrs "port" (cfg.settings.httpd.bindings + ++ cfg.settings.ftpd.bindings + ++ cfg.settings.sftpd.bindings + ++ cfg.settings.webdavd.bindings + ) + ); +in +{ + options.services.sftpgo = { + enable = mkOption { + type = types.bool; + default = false; + description = mdDoc "sftpgo"; + }; + + package = mkOption { + type = types.package; + default = pkgs.sftpgo; + defaultText = literalExpression "pkgs.sftpgo"; + description = mdDoc '' + Which SFTPGo package to use. + ''; + }; + + extraArgs = mkOption { + type = with types; listOf str; + default = []; + description = mdDoc '' + Additional command line arguments to pass to the sftpgo daemon. + ''; + example = [ "--log-level" "info" ]; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/sftpgo"; + description = mdDoc '' + The directory where SFTPGo stores its data files. + ''; + }; + + user = mkOption { + type = types.str; + default = defaultUser; + description = mdDoc '' + User account name under which SFTPGo runs. + ''; + }; + + group = mkOption { + type = types.str; + default = defaultUser; + description = mdDoc '' + Group name under which SFTPGo runs. + ''; + }; + + loadDataFile = mkOption { + default = null; + type = with types; nullOr path; + description = mdDoc '' + Path to a json file containing users and folders to load (or update) on startup. + Check the [documentation](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md) + for the `--loaddata-from` command line argument for more info. + ''; + }; + + settings = mkOption { + default = {}; + description = mdDoc '' + The primary sftpgo configuration. See the + [configuration reference](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md) + for possible values. + ''; + type = with types; submodule { + freeformType = settingsFormat.type; + options = { + httpd.bindings = mkOption { + default = []; + description = mdDoc '' + Configure listen addresses and ports for httpd. + ''; + type = types.listOf (types.submodule { + freeformType = settingsFormat.type; + options = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc '' + Network listen address. Leave blank to listen on all available network interfaces. + On *NIX you can specify an absolute path to listen on a Unix-domain socket. + ''; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = mdDoc '' + The port for serving HTTP(S) requests. + + Setting the port to `0` disables listening on this interface binding. + ''; + }; + + enable_web_admin = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Enable the built-in web admin for this interface binding. + ''; + }; + + enable_web_client = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Enable the built-in web client for this interface binding. + ''; + }; + }; + }); + }; + + ftpd.bindings = mkOption { + default = []; + description = mdDoc '' + Configure listen addresses and ports for ftpd. + ''; + type = types.listOf (types.submodule { + freeformType = settingsFormat.type; + options = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc '' + Network listen address. Leave blank to listen on all available network interfaces. + On *NIX you can specify an absolute path to listen on a Unix-domain socket. + ''; + }; + + port = mkOption { + type = types.port; + default = 0; + description = mdDoc '' + The port for serving FTP requests. + + Setting the port to `0` disables listening on this interface binding. + ''; + }; + }; + }); + }; + + sftpd.bindings = mkOption { + default = []; + description = mdDoc '' + Configure listen addresses and ports for sftpd. + ''; + type = types.listOf (types.submodule { + freeformType = settingsFormat.type; + options = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc '' + Network listen address. Leave blank to listen on all available network interfaces. + On *NIX you can specify an absolute path to listen on a Unix-domain socket. + ''; + }; + + port = mkOption { + type = types.port; + default = 0; + description = mdDoc '' + The port for serving SFTP requests. + + Setting the port to `0` disables listening on this interface binding. + ''; + }; + }; + }); + }; + + webdavd.bindings = mkOption { + default = []; + description = mdDoc '' + Configure listen addresses and ports for webdavd. + ''; + type = types.listOf (types.submodule { + freeformType = settingsFormat.type; + options = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = mdDoc '' + Network listen address. Leave blank to listen on all available network interfaces. + On *NIX you can specify an absolute path to listen on a Unix-domain socket. + ''; + }; + + port = mkOption { + type = types.port; + default = 0; + description = mdDoc '' + The port for serving WebDAV requests. + + Setting the port to `0` disables listening on this interface binding. + ''; + }; + }; + }); + }; + + smtp = mkOption { + default = {}; + description = mdDoc '' + SMTP configuration section. + ''; + type = types.submodule { + freeformType = settingsFormat.type; + options = { + host = mkOption { + type = types.str; + default = ""; + description = mdDoc '' + Location of SMTP email server. Leave empty to disable email sending capabilities. + ''; + }; + + port = mkOption { + type = types.port; + default = 465; + description = mdDoc "Port of the SMTP Server."; + }; + + encryption = mkOption { + type = types.enum [ 0 1 2 ]; + default = 1; + description = mdDoc '' + Encryption scheme: + - `0`: No encryption + - `1`: TLS + - `2`: STARTTLS + ''; + }; + + auth_type = mkOption { + type = types.enum [ 0 1 2 ]; + default = 0; + description = mdDoc '' + - `0`: Plain + - `1`: Login + - `2`: CRAM-MD5 + ''; + }; + + user = mkOption { + type = types.str; + default = "sftpgo"; + description = mdDoc "SMTP username."; + }; + + from = mkOption { + type = types.str; + default = "SFTPGo "; + description = mdDoc '' + From address. + ''; + }; + }; + }; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + services.sftpgo.settings = (mapAttrs (name: mkDefault) { + ftpd.bindings = [{ port = 0; }]; + httpd.bindings = [{ port = 0; }]; + sftpd.bindings = [{ port = 0; }]; + webdavd.bindings = [{ port = 0; }]; + httpd.openapi_path = "${cfg.package}/share/sftpgo/openapi"; + httpd.templates_path = "${cfg.package}/share/sftpgo/templates"; + httpd.static_files_path = "${cfg.package}/share/sftpgo/static"; + smtp.templates_path = "${cfg.package}/share/sftpgo/templates"; + }); + + users = optionalAttrs (cfg.user == defaultUser) { + users = { + ${defaultUser} = { + description = "SFTPGo system user"; + isSystemUser = true; + group = defaultUser; + home = cfg.dataDir; + }; + }; + + groups = { + ${defaultUser} = { + members = [ defaultUser ]; + }; + }; + }; + + systemd.services.sftpgo = { + description = "SFTPGo daemon"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + SFTPGO_CONFIG_FILE = mkDefault configFile; + SFTPGO_LOG_FILE_PATH = mkDefault ""; # log to journal + SFTPGO_LOADDATA_FROM = mkIf (cfg.loadDataFile != null) cfg.loadDataFile; + }; + + serviceConfig = mkMerge [ + ({ + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.dataDir; + ReadWritePaths = [ cfg.dataDir ]; + LimitNOFILE = 8192; # taken from upstream + KillMode = "mixed"; + ExecStart = "${cfg.package}/bin/sftpgo serve ${utils.escapeSystemdExecArgs cfg.extraArgs}"; + ExecReload = "${pkgs.util-linux}/bin/kill -s HUP $MAINPID"; + + # Service hardening + CapabilityBoundingSet = [ (optionalString hasPrivilegedPorts "CAP_NET_BIND_SERVICE") ]; + DevicePolicy = "closed"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + UMask = "0077"; + }) + (mkIf hasPrivilegedPorts { + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + }) + (mkIf (cfg.dataDir == options.services.sftpgo.dataDir.default) { + StateDirectory = baseNameOf cfg.dataDir; + }) + ]; + }; + }; +}