From fc920c4556c9a28cd37b029f1e32e3de8a73d919 Mon Sep 17 00:00:00 2001 From: Sander van der Burg Date: Mon, 13 Sep 2021 22:44:01 +0200 Subject: [PATCH] Add vsftpd service --- services-agnostic/constructors.nix | 10 ++++ services-agnostic/vsftpd/default.nix | 31 +++++++++++ services-agnostic/vsftpd/simple.nix | 78 ++++++++++++++++++++++++++++ tests/default.nix | 4 ++ tests/vsftpd/default.nix | 44 ++++++++++++++++ tests/vsftpd/processes.nix | 54 +++++++++++++++++++ 6 files changed, 221 insertions(+) create mode 100644 services-agnostic/vsftpd/default.nix create mode 100644 services-agnostic/vsftpd/simple.nix create mode 100644 tests/vsftpd/default.nix create mode 100644 tests/vsftpd/processes.nix diff --git a/services-agnostic/constructors.nix b/services-agnostic/constructors.nix index 002f9d7..a74ec9d 100644 --- a/services-agnostic/constructors.nix +++ b/services-agnostic/constructors.nix @@ -197,4 +197,14 @@ in inherit createManagedProcess runtimeDir tmpDir libDir forceDisableUserChange callingUser; inherit (pkgs) lib xinetd writeTextFile; }; + + vsftpd = import ./vsftpd { + inherit createManagedProcess; + inherit (pkgs) vsftpd; + }; + + simpleVsftpd = import ./vsftpd/simple.nix { + inherit createManagedProcess forceDisableUserChange logDir libDir callingUser callingGroup; + inherit (pkgs) stdenv vsftpd writeTextFile lib; + }; } diff --git a/services-agnostic/vsftpd/default.nix b/services-agnostic/vsftpd/default.nix new file mode 100644 index 0000000..011cf46 --- /dev/null +++ b/services-agnostic/vsftpd/default.nix @@ -0,0 +1,31 @@ +{createManagedProcess, vsftpd}: +{instanceSuffix ? "", instanceName ? "vsftpd${instanceSuffix}", initialize ? "", configFile}: + +let + user = instanceName; + group = instanceName; +in +createManagedProcess { + inherit instanceName initialize; + + foregroundProcess = "${vsftpd}/bin/vsftpd"; + args = [ configFile ]; + + credentials = { + groups = { + "${group}" = {}; + }; + users = { + "${user}" = { + inherit group; + description = "vsftpd user"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/services-agnostic/vsftpd/simple.nix b/services-agnostic/vsftpd/simple.nix new file mode 100644 index 0000000..931d6a0 --- /dev/null +++ b/services-agnostic/vsftpd/simple.nix @@ -0,0 +1,78 @@ +{createManagedProcess, stdenv, vsftpd, writeTextFile, lib, logDir, libDir, forceDisableUserChange, callingUser, callingGroup}: + +{ instanceSuffix ? "" +, instanceName ? "vsftpd${instanceSuffix}" +, dataPort ? 20 +, listenPort ? dataPort + 1 +, options ? {} +, enableAnonymousUser ? false +, anonymousUsername ? "ftp" +, anonymousRoot ? if forceDisableUserChange then "/home/${callingUser}" else "/home/${anonymousUsername}" +}: + +let + user = instanceName; + group = instanceName; + + vsftpdLogDir = "${logDir}/${instanceName}"; + + configFile = writeTextFile { + name = "vsftpd.conf"; + text = + lib.optionalString (stdenv.isLinux) '' + seccomp_sandbox=NO + '' + + + '' + vsftpd_log_file=${vsftpdLogDir}/vsftpd.log + xferlog_file=${vsftpdLogDir}/xferlog + '' + + (if forceDisableUserChange then '' + run_as_launching_user=YES + ftp_username=${callingUser} + '' else '' + nopriv_user=${user} + ftp_username=${if enableAnonymousUser then anonymousUsername else "nobody"} + pam_service_name=vsftpd + secure_chroot_dir=/var/empty + '') + + '' + ftp_data_port=${toString dataPort} + listen_port=${toString listenPort} + '' + + lib.optionalString enableAnonymousUser '' + anon_root=${anonymousRoot} + '' + + lib.concatMapStrings (name: + let + value = builtins.getAttr name options; + in + "${name}=${toString value}\n" + ) (builtins.attrNames options); + }; +in +import ./default.nix { + inherit createManagedProcess vsftpd; +} { + inherit instanceSuffix instanceName; + + # When running as unprivileged user, we need to make a copy of the config file and make the calling user the owner + configFile = if forceDisableUserChange then "${libDir}/${instanceName}/vsftpd.conf" else configFile; + + initialize = + '' + mkdir -p ${vsftpdLogDir} + '' + + + # Make the unprivileged user the owner of the config file + lib.optionalString forceDisableUserChange + (let + dynamicConfigFile = "${libDir}/${instanceName}/vsftpd.conf"; + in + '' + mkdir -p ${libDir}/${instanceName} + cp ${configFile} ${dynamicConfigFile} + chmod u+w ${dynamicConfigFile} + chown ${callingUser}:${callingGroup} ${dynamicConfigFile} + ''); +} diff --git a/tests/default.nix b/tests/default.nix index 829fa32..32f16b4 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -104,4 +104,8 @@ in xinetd-extendable = import ./xinetd/extendable { inherit pkgs processManagers profiles testService nix-processmgmt; }; + + vsftpd = import ./vsftpd { + inherit pkgs processManagers profiles testService nix-processmgmt; + }; } diff --git a/tests/vsftpd/default.nix b/tests/vsftpd/default.nix new file mode 100644 index 0000000..703a20e --- /dev/null +++ b/tests/vsftpd/default.nix @@ -0,0 +1,44 @@ +{ pkgs, testService, processManagers, profiles, nix-processmgmt }: + +testService { + exprFile = ./processes.nix; + extraParams = { + inherit nix-processmgmt; + }; + + nixosConfig = { + users.users.ftp = { + description = "Anonymous FTP user"; + isNormalUser = true; + createHome = true; + password = "secret"; + }; + }; + + systemPackages = [ pkgs.inetutils ]; + + readiness = {instanceName, instance, ...}: + '' + machine.wait_for_open_port(${toString instance.listenPort}) + ''; + + tests = {instanceName, instance, forceDisableUserChange, ...}: + if forceDisableUserChange then '' + machine.succeed("echo test > /home/unprivileged/test.txt") + machine.succeed("chown unprivileged:users /home/unprivileged/test.txt") + machine.succeed('(echo "user anonymous foobar"; echo "ls") | ftp -n 127.0.0.1 ${toString instance.listenPort} >&2') + machine.succeed("curl --fail ftp://anonymous@localhost:${toString instance.listenPort}/test.txt -o test.txt") + machine.succeed("grep test test.txt") + machine.succeed("rm test.txt") + '' else '' + machine.succeed("echo test > /home/ftp/test.txt") + machine.succeed("chown ftp:users /home/ftp/test.txt") + machine.succeed("chmod a-w /home/ftp") + machine.succeed('(echo "user anonymous foobar"; echo "ls") | ftp -n 127.0.0.1 ${pkgs.lib.optionalString (instance.listenPort != 21) (toString instance.listenPort)} >&2') + machine.succeed("curl -v --fail ftp://anonymous@localhost${pkgs.lib.optionalString (instance.listenPort != 21) ":${toString instance.listenPort}"}/test.txt -o test.txt 2>&1") + machine.succeed("grep test test.txt") + machine.succeed("rm test.txt") + ''; + + inherit processManagers profiles; +} diff --git a/tests/vsftpd/processes.nix b/tests/vsftpd/processes.nix new file mode 100644 index 0000000..906f613 --- /dev/null +++ b/tests/vsftpd/processes.nix @@ -0,0 +1,54 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, spoolDir ? "${stateDir}/spool" +, cacheDir ? "${stateDir}/cache" +, libDir ? "${stateDir}/lib" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, callingUser ? null +, callingGroup ? null +, processManager +, nix-processmgmt ? ../../../nix-processmgmt +}: + +let + constructors = import ../../services-agnostic/constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir cacheDir libDir spoolDir forceDisableUserChange callingUser callingGroup processManager nix-processmgmt; + }; +in +{ + vsftpd = rec { + dataPort = if forceDisableUserChange then 2000 else 20; + listenPort = if forceDisableUserChange then 2001 else 21; + + pkg = constructors.simpleVsftpd { + inherit dataPort listenPort; + enableAnonymousUser = true; + options = { + dual_log_enable = "YES"; + local_enable = "YES"; + anon_world_readable_only = "NO"; + }; + }; + }; + + vsftpd-secondary = rec { + dataPort = if forceDisableUserChange then 2010 else 30; + listenPort = if forceDisableUserChange then 2011 else 31; + + pkg = constructors.simpleVsftpd { + inherit dataPort listenPort; + enableAnonymousUser = true; + + instanceSuffix = "-secondary"; + options = { + dual_log_enable = "YES"; + local_enable = "YES"; + anon_world_readable_only = "NO"; + }; + }; + }; +}