1{
2 options,
3 config,
4 lib,
5 pkgs,
6 utils,
7 ...
8}:
9
10with lib;
11
12let
13 cfg = config.services.sftpgo;
14 defaultUser = "sftpgo";
15 settingsFormat = pkgs.formats.json { };
16 configFile = settingsFormat.generate "sftpgo.json" cfg.settings;
17 hasPrivilegedPorts = any (port: port > 0 && port < 1024) (
18 catAttrs "port" (
19 cfg.settings.httpd.bindings
20 ++ cfg.settings.ftpd.bindings
21 ++ cfg.settings.sftpd.bindings
22 ++ cfg.settings.webdavd.bindings
23 )
24 );
25in
26{
27 options.services.sftpgo = {
28 enable = mkOption {
29 type = types.bool;
30 default = false;
31 description = "sftpgo";
32 };
33
34 package = mkPackageOption pkgs "sftpgo" { };
35
36 extraArgs = mkOption {
37 type = with types; listOf str;
38 default = [ ];
39 description = ''
40 Additional command line arguments to pass to the sftpgo daemon.
41 '';
42 example = [
43 "--log-level"
44 "info"
45 ];
46 };
47
48 dataDir = mkOption {
49 type = types.path;
50 default = "/var/lib/sftpgo";
51 description = ''
52 The directory where SFTPGo stores its data files.
53 '';
54 };
55
56 extraReadWriteDirs = mkOption {
57 type = types.listOf types.path;
58 default = [ ];
59 description = ''
60 Extra directories where SFTPGo is allowed to write to.
61 '';
62 };
63
64 user = mkOption {
65 type = types.str;
66 default = defaultUser;
67 description = ''
68 User account name under which SFTPGo runs.
69 '';
70 };
71
72 group = mkOption {
73 type = types.str;
74 default = defaultUser;
75 description = ''
76 Group name under which SFTPGo runs.
77 '';
78 };
79
80 loadDataFile = mkOption {
81 default = null;
82 type = with types; nullOr path;
83 description = ''
84 Path to a json file containing users and folders to load (or update) on startup.
85 Check the [documentation](https://sftpgo.github.io/latest/config-file/)
86 for the `--loaddata-from` command line argument for more info.
87 '';
88 };
89
90 settings = mkOption {
91 default = { };
92 description = ''
93 The primary sftpgo configuration. See the
94 [configuration reference](https://sftpgo.github.io/latest/config-file/)
95 for possible values.
96 '';
97 type =
98 with types;
99 submodule {
100 freeformType = settingsFormat.type;
101 options = {
102 httpd.bindings = mkOption {
103 default = [ ];
104 description = ''
105 Configure listen addresses and ports for httpd.
106 '';
107 type = types.listOf (
108 types.submodule {
109 freeformType = settingsFormat.type;
110 options = {
111 address = mkOption {
112 type = types.str;
113 default = "127.0.0.1";
114 description = ''
115 Network listen address. Leave blank to listen on all available network interfaces.
116 On *NIX you can specify an absolute path to listen on a Unix-domain socket.
117 '';
118 };
119
120 port = mkOption {
121 type = types.port;
122 default = 8080;
123 description = ''
124 The port for serving HTTP(S) requests.
125
126 Setting the port to `0` disables listening on this interface binding.
127 '';
128 };
129
130 enable_web_admin = mkOption {
131 type = types.bool;
132 default = true;
133 description = ''
134 Enable the built-in web admin for this interface binding.
135 '';
136 };
137
138 enable_web_client = mkOption {
139 type = types.bool;
140 default = true;
141 description = ''
142 Enable the built-in web client for this interface binding.
143 '';
144 };
145 };
146 }
147 );
148 };
149
150 ftpd.bindings = mkOption {
151 default = [ ];
152 description = ''
153 Configure listen addresses and ports for ftpd.
154 '';
155 type = types.listOf (
156 types.submodule {
157 freeformType = settingsFormat.type;
158 options = {
159 address = mkOption {
160 type = types.str;
161 default = "127.0.0.1";
162 description = ''
163 Network listen address. Leave blank to listen on all available network interfaces.
164 On *NIX you can specify an absolute path to listen on a Unix-domain socket.
165 '';
166 };
167
168 port = mkOption {
169 type = types.port;
170 default = 0;
171 description = ''
172 The port for serving FTP requests.
173
174 Setting the port to `0` disables listening on this interface binding.
175 '';
176 };
177 };
178 }
179 );
180 };
181
182 sftpd.bindings = mkOption {
183 default = [ ];
184 description = ''
185 Configure listen addresses and ports for sftpd.
186 '';
187 type = types.listOf (
188 types.submodule {
189 freeformType = settingsFormat.type;
190 options = {
191 address = mkOption {
192 type = types.str;
193 default = "127.0.0.1";
194 description = ''
195 Network listen address. Leave blank to listen on all available network interfaces.
196 On *NIX you can specify an absolute path to listen on a Unix-domain socket.
197 '';
198 };
199
200 port = mkOption {
201 type = types.port;
202 default = 0;
203 description = ''
204 The port for serving SFTP requests.
205
206 Setting the port to `0` disables listening on this interface binding.
207 '';
208 };
209 };
210 }
211 );
212 };
213
214 webdavd.bindings = mkOption {
215 default = [ ];
216 description = ''
217 Configure listen addresses and ports for webdavd.
218 '';
219 type = types.listOf (
220 types.submodule {
221 freeformType = settingsFormat.type;
222 options = {
223 address = mkOption {
224 type = types.str;
225 default = "127.0.0.1";
226 description = ''
227 Network listen address. Leave blank to listen on all available network interfaces.
228 On *NIX you can specify an absolute path to listen on a Unix-domain socket.
229 '';
230 };
231
232 port = mkOption {
233 type = types.port;
234 default = 0;
235 description = ''
236 The port for serving WebDAV requests.
237
238 Setting the port to `0` disables listening on this interface binding.
239 '';
240 };
241 };
242 }
243 );
244 };
245
246 smtp = mkOption {
247 default = { };
248 description = ''
249 SMTP configuration section.
250 '';
251 type = types.submodule {
252 freeformType = settingsFormat.type;
253 options = {
254 host = mkOption {
255 type = types.str;
256 default = "";
257 description = ''
258 Location of SMTP email server. Leave empty to disable email sending capabilities.
259 '';
260 };
261
262 port = mkOption {
263 type = types.port;
264 default = 465;
265 description = "Port of the SMTP Server.";
266 };
267
268 encryption = mkOption {
269 type = types.enum [
270 0
271 1
272 2
273 ];
274 default = 1;
275 description = ''
276 Encryption scheme:
277 - `0`: No encryption
278 - `1`: TLS
279 - `2`: STARTTLS
280 '';
281 };
282
283 auth_type = mkOption {
284 type = types.enum [
285 0
286 1
287 2
288 ];
289 default = 0;
290 description = ''
291 - `0`: Plain
292 - `1`: Login
293 - `2`: CRAM-MD5
294 '';
295 };
296
297 user = mkOption {
298 type = types.str;
299 default = "sftpgo";
300 description = "SMTP username.";
301 };
302
303 from = mkOption {
304 type = types.str;
305 default = "SFTPGo <sftpgo@example.com>";
306 description = ''
307 From address.
308 '';
309 };
310 };
311 };
312 };
313 };
314 };
315 };
316 };
317
318 config = mkIf cfg.enable {
319 services.sftpgo.settings = (
320 mapAttrs (name: mkDefault) {
321 ftpd.bindings = [ { port = 0; } ];
322 httpd.bindings = [ { port = 0; } ];
323 sftpd.bindings = [ { port = 0; } ];
324 webdavd.bindings = [ { port = 0; } ];
325 httpd.openapi_path = "${cfg.package}/share/sftpgo/openapi";
326 httpd.templates_path = "${cfg.package}/share/sftpgo/templates";
327 httpd.static_files_path = "${cfg.package}/share/sftpgo/static";
328 smtp.templates_path = "${cfg.package}/share/sftpgo/templates";
329 }
330 );
331
332 users = optionalAttrs (cfg.user == defaultUser) {
333 users = {
334 ${defaultUser} = {
335 description = "SFTPGo system user";
336 isSystemUser = true;
337 group = defaultUser;
338 home = cfg.dataDir;
339 };
340 };
341
342 groups = {
343 ${defaultUser} = {
344 members = [ defaultUser ];
345 };
346 };
347 };
348
349 systemd.services.sftpgo = {
350 description = "SFTPGo daemon";
351 after = [ "network.target" ];
352 wantedBy = [ "multi-user.target" ];
353
354 environment = {
355 SFTPGO_CONFIG_FILE = mkDefault configFile;
356 SFTPGO_LOG_FILE_PATH = mkDefault ""; # log to journal
357 SFTPGO_LOADDATA_FROM = mkIf (cfg.loadDataFile != null) cfg.loadDataFile;
358 };
359
360 serviceConfig = mkMerge [
361 ({
362 Type = "simple";
363 User = cfg.user;
364 Group = cfg.group;
365 WorkingDirectory = cfg.dataDir;
366 ReadWritePaths = [ cfg.dataDir ] ++ cfg.extraReadWriteDirs;
367 LimitNOFILE = 8192; # taken from upstream
368 KillMode = "mixed";
369 ExecStart = "${cfg.package}/bin/sftpgo serve ${utils.escapeSystemdExecArgs cfg.extraArgs}";
370 ExecReload = "${pkgs.util-linux}/bin/kill -s HUP $MAINPID";
371
372 # Service hardening
373 CapabilityBoundingSet = [ (optionalString hasPrivilegedPorts "CAP_NET_BIND_SERVICE") ];
374 DevicePolicy = "closed";
375 LockPersonality = true;
376 NoNewPrivileges = true;
377 PrivateDevices = true;
378 PrivateTmp = true;
379 ProcSubset = "pid";
380 ProtectClock = true;
381 ProtectControlGroups = true;
382 ProtectHome = true;
383 ProtectHostname = true;
384 ProtectKernelLogs = true;
385 ProtectKernelModules = true;
386 ProtectKernelTunables = true;
387 ProtectProc = "invisible";
388 ProtectSystem = "strict";
389 RemoveIPC = true;
390 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
391 RestrictNamespaces = true;
392 RestrictRealtime = true;
393 RestrictSUIDSGID = true;
394 SystemCallArchitectures = "native";
395 SystemCallFilter = [
396 "@system-service"
397 "~@privileged"
398 ];
399 UMask = "0077";
400 })
401 (mkIf hasPrivilegedPorts {
402 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
403 })
404 (mkIf (cfg.dataDir == options.services.sftpgo.dataDir.default) {
405 StateDirectory = baseNameOf cfg.dataDir;
406 })
407 ];
408 };
409 };
410}