From 52b16dc40d15dddafbd002b073f570b85c3e17f0 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 2 Apr 2026 16:08:05 +0100 Subject: [PATCH] nixos/tusd: init module --- nixos/modules/module-list.nix | 1 + nixos/modules/services/web-servers/tusd.nix | 162 ++++++++++++++++++++ nixos/tests/tusd/default.nix | 38 ++--- 3 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 nixos/modules/services/web-servers/tusd.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3347adb1d145..74d449245deb 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1825,6 +1825,7 @@ ./services/web-servers/traefik.nix ./services/web-servers/trafficserver/default.nix ./services/web-servers/ttyd.nix + ./services/web-servers/tusd.nix ./services/web-servers/unit/default.nix ./services/web-servers/uwsgi.nix ./services/web-servers/varnish/default.nix diff --git a/nixos/modules/services/web-servers/tusd.nix b/nixos/modules/services/web-servers/tusd.nix new file mode 100644 index 000000000000..4c81491b1f2f --- /dev/null +++ b/nixos/modules/services/web-servers/tusd.nix @@ -0,0 +1,162 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.tusd; + username = "tusd"; + groupname = "tusd"; + + args = [ + "-host=${cfg.host}" + "-port=${toString cfg.port}" + "-base-path=${cfg.basePath}" + "-upload-dir=${cfg.uploadDir}" + ] + ++ lib.optional cfg.behindProxy "-behind-proxy" + ++ lib.optional (cfg.maxSize != null) "-max-size=${toString cfg.maxSize}" + ++ lib.optional (cfg.networkTimeout != null) "-network-timeout=${cfg.networkTimeout}" + ++ lib.optional (cfg.hooksHttp != null) "-hooks-http=${cfg.hooksHttp}" + ++ lib.optional ( + cfg.hooksEnabledEvents != [ ] + ) "-hooks-enabled-events=${lib.concatStringsSep "," cfg.hooksEnabledEvents}" + ++ cfg.extraArgs; +in +{ + meta.maintainers = with lib.maintainers; [ m1-s ]; + + options.services.tusd = { + enable = lib.mkEnableOption "tus resumable upload protocol server"; + + host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "The host to bind the HTTP server to."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "The port to bind the HTTP server to."; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open the firewall port for tusd."; + }; + + basePath = lib.mkOption { + type = lib.types.str; + default = "/files/"; + description = "The basepath of the HTTP server."; + }; + + uploadDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/tusd/data"; + description = "The directory to store uploads in."; + }; + + behindProxy = lib.mkEnableOption null // { + description = "Whether to respect X-Forwarded-* and similar headers which may be set by proxies."; + }; + + maxSize = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "The maximum size of a single upload in bytes."; + }; + + networkTimeout = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + The timeout for reading the request and writing the response. + If tusd does not receive data for this duration, + it will consider the connection dead. + ''; + example = "30s"; + }; + + hooksHttp = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The HTTP endpoint to which hook events will be sent to."; + example = "http://localhost:8081/hooks"; + }; + + hooksEnabledEvents = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "The list of enabled hook events."; + example = [ + "pre-create" + "post-finish" + ]; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Additional arguments given to tusd."; + example = [ + "-verbose" + "-log-format=json" + ]; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.${username} = { + isSystemUser = true; + group = groupname; + }; + users.groups.${groupname} = { }; + + # tusd knows how to create subdirectories in this folder but we have to + # create the root folder ourselves. + systemd.tmpfiles.settings."tusd".${cfg.uploadDir}.d = { + user = username; + group = groupname; + # default taken from https://github.com/tus/tusd/blob/55a096a10942b85360664a1e8aea7bd758272053/pkg/filestore/filestore.go#L37 + mode = "0775"; + }; + + systemd.services.tusd = { + description = "tusd - tus resumable upload protocol server"; + documentation = [ "https://github.com/tus/tusd" ]; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + User = username; + Group = groupname; + + ExecStart = lib.escapeShellArgs ([ (lib.getExe pkgs.tusd) ] ++ args); + Restart = "on-failure"; + + StateDirectory = "tusd"; + + # Hardening + LockPersonality = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostUserNamespaces = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port; + }; +} diff --git a/nixos/tests/tusd/default.nix b/nixos/tests/tusd/default.nix index ec188dee56ca..1e21ec009662 100644 --- a/nixos/tests/tusd/default.nix +++ b/nixos/tests/tusd/default.nix @@ -2,37 +2,28 @@ let port = 1080; - - client = - { pkgs, ... }: - { - environment.systemPackages = [ pkgs.curl ]; - }; - - server = - { pkgs, ... }: - { - # tusd does not have a NixOS service yet. - systemd.services.tusd = { - wantedBy = [ "multi-user.target" ]; - - serviceConfig = { - ExecStart = ''${pkgs.tusd}/bin/tusd -port "${toString port}" -upload-dir=/data''; - }; - }; - networking.firewall.allowedTCPPorts = [ port ]; - }; + uploadDir = "/var/lib/tusd/data"; in { name = "tusd"; meta.maintainers = with lib.maintainers; [ + m1-s nh2 kalbasit ]; nodes = { - inherit server; - inherit client; + client = { + environment.systemPackages = [ pkgs.curl ]; + }; + + server = { + services.tusd = { + enable = true; + inherit port uploadDir; + openFirewall = true; + }; + }; }; testScript = '' @@ -46,6 +37,9 @@ in client.wait_for_unit("network.target") client.succeed("${./tus-curl-upload.sh} file-100M.bin http://server:${toString port}/files/") + # Verify file was created in uploadDir + server.succeed("test -n \"$(ls -A ${uploadDir})\"") + print("Upload succeeded") ''; }