diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml index 5de7e79bb08..e9f4f707f52 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml @@ -314,6 +314,13 @@ services.alps. + + + endlessh, + an SSH tarpit. Available as + services.endlessh. + + endlessh-go, diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md index 541deabc205..9b5257ca8e1 100644 --- a/nixos/doc/manual/release-notes/rl-2211.section.md +++ b/nixos/doc/manual/release-notes/rl-2211.section.md @@ -109,6 +109,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [alps](https://git.sr.ht/~migadu/alps), a simple and extensible webmail. Available as [services.alps](#opt-services.alps.enable). +- [endlessh](https://github.com/skeeto/endlessh), an SSH tarpit. Available as [services.endlessh](#opt-services.endlessh.enable). + - [endlessh-go](https://github.com/shizunge/endlessh-go), an SSH tarpit that exposes Prometheus metrics. Available as [services.endlessh-go](#opt-services.endlessh-go.enable). - [Garage](https://garagehq.deuxfleurs.fr/), a simple object storage server for geodistributed deployments, alternative to MinIO. Available as [services.garage](#opt-services.garage.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ba9f67f87fd..1e9298a6571 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1012,6 +1012,7 @@ ./services/security/certmgr.nix ./services/security/cfssl.nix ./services/security/clamav.nix + ./services/security/endlessh.nix ./services/security/endlessh-go.nix ./services/security/fail2ban.nix ./services/security/fprintd.nix diff --git a/nixos/modules/services/security/endlessh.nix b/nixos/modules/services/security/endlessh.nix new file mode 100644 index 00000000000..e99b4dadcd5 --- /dev/null +++ b/nixos/modules/services/security/endlessh.nix @@ -0,0 +1,99 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.endlessh; +in +{ + options.services.endlessh = { + enable = mkEnableOption (mdDoc "endlessh service"); + + port = mkOption { + type = types.port; + default = 2222; + example = 22; + description = mdDoc '' + Specifies on which port the endlessh daemon listens for SSH + connections. + + Setting this to `22` may conflict with {option}`services.openssh`. + ''; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "-6" "-d 9000" "-v" ]; + description = mdDoc '' + Additional command line options to pass to the endlessh daemon. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to open a firewall port for the SSH listener. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.endlessh = { + description = "SSH tarpit"; + requires = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + let + needsPrivileges = cfg.port < 1024; + capabilities = [ "" ] ++ optionals needsPrivileges [ "CAP_NET_BIND_SERVICE" ]; + rootDirectory = "/run/endlessh"; + in + { + Restart = "always"; + ExecStart = with cfg; concatStringsSep " " ([ + "${pkgs.endlessh}/bin/endlessh" + "-p ${toString port}" + ] ++ extraOptions); + DynamicUser = true; + RootDirectory = rootDirectory; + BindReadOnlyPaths = [ builtins.storeDir ]; + InaccessiblePaths = [ "-+${rootDirectory}" ]; + RuntimeDirectory = baseNameOf rootDirectory; + RuntimeDirectoryMode = "700"; + AmbientCapabilities = capabilities; + CapabilityBoundingSet = capabilities; + UMask = "0077"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = !needsPrivileges; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + ProtectProc = "noaccess"; + ProcSubset = "pid"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ]; + }; + }; + + networking.firewall.allowedTCPPorts = with cfg; + optionals openFirewall [ port ]; + }; + + meta.maintainers = with maintainers; [ azahi ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 06ee955668f..a3253339c03 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -183,6 +183,7 @@ in { ejabberd = handleTest ./xmpp/ejabberd.nix {}; elk = handleTestOn ["x86_64-linux"] ./elk.nix {}; emacs-daemon = handleTest ./emacs-daemon.nix {}; + endlessh = handleTest ./endlessh.nix {}; endlessh-go = handleTest ./endlessh-go.nix {}; engelsystem = handleTest ./engelsystem.nix {}; enlightenment = handleTest ./enlightenment.nix {}; diff --git a/nixos/tests/endlessh.nix b/nixos/tests/endlessh.nix new file mode 100644 index 00000000000..be742a749fd --- /dev/null +++ b/nixos/tests/endlessh.nix @@ -0,0 +1,43 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: +{ + name = "endlessh"; + meta.maintainers = with lib.maintainers; [ azahi ]; + + nodes = { + server = { ... }: { + services.endlessh = { + enable = true; + openFirewall = true; + }; + + specialisation = { + unprivileged.configuration.services.endlessh.port = 2222; + + privileged.configuration.services.endlessh.port = 22; + }; + }; + + client = { pkgs, ... }: { + environment.systemPackages = with pkgs; [ curl netcat ]; + }; + }; + + testScript = '' + def activate_specialisation(name: str): + server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2") + + start_all() + + with subtest("Unprivileged"): + activate_specialisation("unprivileged") + server.wait_for_unit("endlessh.service") + server.wait_for_open_port(2222) + client.succeed("nc -dvW5 server 2222") + + with subtest("Privileged"): + activate_specialisation("privileged") + server.wait_for_unit("endlessh.service") + server.wait_for_open_port(22) + client.succeed("nc -dvW5 server 22") + ''; +}) diff --git a/pkgs/servers/endlessh/default.nix b/pkgs/servers/endlessh/default.nix index fc05ec033d4..e408c764939 100644 --- a/pkgs/servers/endlessh/default.nix +++ b/pkgs/servers/endlessh/default.nix @@ -1,4 +1,10 @@ -{ lib, stdenv, fetchFromGitHub }: +{ lib +, stdenv +, fetchFromGitHub +, testers +, endlessh +, nixosTests +}: stdenv.mkDerivation rec { pname = "endlessh"; @@ -8,17 +14,25 @@ stdenv.mkDerivation rec { owner = "skeeto"; repo = pname; rev = version; - sha256 = "0ziwr8j1frsp3dajr8h5glkm1dn5cci404kazz5w1jfrp0736x68"; + hash = "sha256-yHQzDrjZycDL/2oSQCJjxbZQJ30FoixVG1dnFyTKPH4="; }; makeFlags = [ "PREFIX=$(out)" ]; + passthru.tests = { + inherit (nixosTests) endlessh; + version = testers.testVersion { + package = endlessh; + command = "endlessh -V"; + }; + }; + meta = with lib; { description = "SSH tarpit that slowly sends an endless banner"; homepage = "https://github.com/skeeto/endlessh"; changelog = "https://github.com/skeeto/endlessh/releases/tag/${version}"; license = licenses.unlicense; - maintainers = [ maintainers.marsam ]; + maintainers = with maintainers; [ azahi marsam ]; platforms = platforms.unix; }; }