From 4c707039315ab2a276339f1ceb33dc1477f2d37f Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Wed, 31 Jan 2024 17:54:32 +0100 Subject: [PATCH 1/3] nixos/ttyd: add writable option Co-authored-by: Carsten Rodin <19612711+carstoid@users.noreply.github.com> --- nixos/modules/services/web-servers/ttyd.nix | 10 ++++++++++ nixos/tests/web-servers/ttyd.nix | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix index e545869ca432..9315890d5c8d 100644 --- a/nixos/modules/services/web-servers/ttyd.nix +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -14,6 +14,7 @@ let ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) ++ [ "--terminal-type" cfg.terminalType ] ++ optionals cfg.checkOrigin [ "--check-origin" ] + ++ optionals cfg.writeable [ "--writable" ] # the typo is correct ++ [ "--max-clients" (toString cfg.maxClients) ] ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] ++ optionals cfg.enableIPv6 [ "--ipv6" ] @@ -75,6 +76,13 @@ in description = lib.mdDoc "Signal to send to the command on session close."; }; + writeable = mkOption { + type = types.nullOr types.bool; + default = null; # null causes an eval error, forcing the user to consider attack surface + example = true; + description = lib.mdDoc "Allow clients to write to the TTY."; + }; + clientOptions = mkOption { type = types.attrsOf types.str; default = {}; @@ -165,6 +173,8 @@ in [ { assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specified."; } + { assertion = cfg.writeable != null; + message = "services.ttyd.writeable must be set"; } { assertion = ! (cfg.interface != null && cfg.socket != null); message = "Cannot set both interface and socket for ttyd."; } { assertion = (cfg.username != null) == (cfg.passwordFile != null); diff --git a/nixos/tests/web-servers/ttyd.nix b/nixos/tests/web-servers/ttyd.nix index d161673684b3..739ebc3aac6e 100644 --- a/nixos/tests/web-servers/ttyd.nix +++ b/nixos/tests/web-servers/ttyd.nix @@ -2,18 +2,29 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: { name = "ttyd"; meta.maintainers = with lib.maintainers; [ stunkymonkey ]; - nodes.machine = { pkgs, ... }: { + nodes.readonly = { pkgs, ... }: { services.ttyd = { enable = true; username = "foo"; passwordFile = pkgs.writeText "password" "bar"; + writeable = false; + }; + }; + + nodes.writeable = { pkgs, ... }: { + services.ttyd = { + enable = true; + username = "foo"; + passwordFile = pkgs.writeText "password" "bar"; + writeable = true; }; }; testScript = '' - machine.wait_for_unit("ttyd.service") - machine.wait_for_open_port(7681) - response = machine.succeed("curl -vvv -u foo:bar -s -H 'Host: ttyd' http://127.0.0.1:7681/") - assert 'ttyd - Terminal' in response, "Page didn't load successfully" + for machine in [readonly, writeable]: + machine.wait_for_unit("ttyd.service") + machine.wait_for_open_port(7681) + response = machine.succeed("curl -vvv -u foo:bar -s -H 'Host: ttyd' http://127.0.0.1:7681/") + assert 'ttyd - Terminal' in response, "Page didn't load successfully" ''; }) From 0d13d2a90f21947ddc20c13c4b0a70d88c354b16 Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Wed, 31 Jan 2024 17:58:59 +0100 Subject: [PATCH 2/3] nixos/ttyd: remove `with lib;` --- nixos/modules/services/web-servers/ttyd.nix | 54 ++++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix index 9315890d5c8d..1b7db0faff9f 100644 --- a/nixos/modules/services/web-servers/ttyd.nix +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -1,11 +1,17 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.services.ttyd; + inherit (lib) + optionals + types + concatLists + mapAttrsToList + mkOption + ; + # Command line arguments for the ttyd daemon args = [ "--port" (toString cfg.port) ] ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] @@ -31,39 +37,39 @@ in options = { services.ttyd = { - enable = mkEnableOption (lib.mdDoc "ttyd daemon"); + enable = lib.mkEnableOption ("ttyd daemon"); port = mkOption { type = types.port; default = 7681; - description = lib.mdDoc "Port to listen on (use 0 for random port)"; + description = "Port to listen on (use 0 for random port)"; }; socket = mkOption { type = types.nullOr types.path; default = null; example = "/var/run/ttyd.sock"; - description = lib.mdDoc "UNIX domain socket path to bind."; + description = "UNIX domain socket path to bind."; }; interface = mkOption { type = types.nullOr types.str; default = null; example = "eth0"; - description = lib.mdDoc "Network interface to bind."; + description = "Network interface to bind."; }; username = mkOption { type = types.nullOr types.str; default = null; - description = lib.mdDoc "Username for basic authentication."; + description = "Username for basic authentication."; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; - description = lib.mdDoc '' + description = '' File containing the password to use for basic authentication. For insecurely putting the password in the globally readable store use `pkgs.writeText "ttydpw" "MyPassword"`. @@ -73,26 +79,26 @@ in signal = mkOption { type = types.ints.u8; default = 1; - description = lib.mdDoc "Signal to send to the command on session close."; + description = "Signal to send to the command on session close."; }; writeable = mkOption { type = types.nullOr types.bool; default = null; # null causes an eval error, forcing the user to consider attack surface example = true; - description = lib.mdDoc "Allow clients to write to the TTY."; + description = "Allow clients to write to the TTY."; }; clientOptions = mkOption { type = types.attrsOf types.str; default = {}; - example = literalExpression '' + example = lib.literalExpression '' { fontSize = "16"; fontFamily = "Fira Code"; } ''; - description = lib.mdDoc '' + description = '' Attribute set of client options for xtermjs. ''; @@ -101,50 +107,50 @@ in terminalType = mkOption { type = types.str; default = "xterm-256color"; - description = lib.mdDoc "Terminal type to report."; + description = "Terminal type to report."; }; checkOrigin = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether to allow a websocket connection from a different origin."; + description = "Whether to allow a websocket connection from a different origin."; }; maxClients = mkOption { type = types.int; default = 0; - description = lib.mdDoc "Maximum clients to support (0, no limit)"; + description = "Maximum clients to support (0, no limit)"; }; indexFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "Custom index.html path"; + description = "Custom index.html path"; }; enableIPv6 = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether or not to enable IPv6 support."; + description = "Whether or not to enable IPv6 support."; }; enableSSL = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether or not to enable SSL (https) support."; + description = "Whether or not to enable SSL (https) support."; }; certFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "SSL certificate file path."; + description = "SSL certificate file path."; }; keyFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; - description = lib.mdDoc '' + description = '' SSL key file path. For insecurely putting the keyFile in the globally readable store use `pkgs.writeText "ttydKeyFile" "SSLKEY"`. @@ -154,20 +160,20 @@ in caFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "SSL CA file path for client certificate verification."; + description = "SSL CA file path for client certificate verification."; }; logLevel = mkOption { type = types.int; default = 7; - description = lib.mdDoc "Set log level."; + description = "Set log level."; }; }; }; ###### implementation - config = mkIf cfg.enable { + config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.enableSSL @@ -196,7 +202,7 @@ in script = if cfg.passwordFile != null then '' PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ - --credential ${escapeShellArg cfg.username}:"$PASSWORD" \ + --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \ ${pkgs.shadow}/bin/login '' else '' From a8880f1647e6b8e83f1f9909bea17d4c0dbe8428 Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Wed, 31 Jan 2024 17:59:39 +0100 Subject: [PATCH 3/3] nixos/ttyd: add entrypoint option --- nixos/modules/services/web-servers/ttyd.nix | 32 ++++++++++++++++----- nixos/tests/web-servers/ttyd.nix | 3 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix index 1b7db0faff9f..14361df2bb66 100644 --- a/nixos/modules/services/web-servers/ttyd.nix +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -62,7 +62,7 @@ in username = mkOption { type = types.nullOr types.str; default = null; - description = "Username for basic authentication."; + description = "Username for basic http authentication."; }; passwordFile = mkOption { @@ -70,7 +70,7 @@ in default = null; apply = value: if value == null then null else toString value; description = '' - File containing the password to use for basic authentication. + File containing the password to use for basic http authentication. For insecurely putting the password in the globally readable store use `pkgs.writeText "ttydpw" "MyPassword"`. ''; @@ -82,6 +82,26 @@ in description = "Signal to send to the command on session close."; }; + entrypoint = mkOption { + type = types.listOf types.str; + default = [ "${pkgs.shadow}/bin/login" ]; + defaultText = lib.literalExpression '' + [ "''${pkgs.shadow}/bin/login" ] + ''; + example = lib.literalExpression '' + [ (lib.getExe pkgs.htop) ] + ''; + description = "Which command ttyd runs."; + apply = lib.escapeShellArgs; + }; + + user = mkOption { + type = types.str; + # `login` needs to be run as root + default = "root"; + description = "Which unix user ttyd should run as."; + }; + writeable = mkOption { type = types.nullOr types.bool; default = null; # null causes an eval error, forcing the user to consider attack surface @@ -193,9 +213,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { - # Runs login which needs to be run as root - # login: Cannot possibly work without effective root - User = "root"; + User = cfg.user; LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; }; @@ -203,11 +221,11 @@ in PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \ - ${pkgs.shadow}/bin/login + ${cfg.entrypoint} '' else '' ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ - ${pkgs.shadow}/bin/login + ${cfg.entrypoint} ''; }; }; diff --git a/nixos/tests/web-servers/ttyd.nix b/nixos/tests/web-servers/ttyd.nix index 739ebc3aac6e..b79a2032ec75 100644 --- a/nixos/tests/web-servers/ttyd.nix +++ b/nixos/tests/web-servers/ttyd.nix @@ -5,8 +5,7 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: { nodes.readonly = { pkgs, ... }: { services.ttyd = { enable = true; - username = "foo"; - passwordFile = pkgs.writeText "password" "bar"; + entrypoint = [ (lib.getExe pkgs.htop) ]; writeable = false; }; };