From 40f2327242db3ec620dc375f8a4c8e90c6bd3f8b Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 25 Jun 2026 15:59:16 +0200 Subject: [PATCH 1/8] Add selectable shell and session backend options --- ssh/config.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ssh/config.yaml b/ssh/config.yaml index 13668960..9bf6fd2f 100644 --- a/ssh/config.yaml +++ b/ssh/config.yaml @@ -63,7 +63,9 @@ options: allow_agent_forwarding: false allow_remote_port_forwarding: false allow_tcp_forwarding: false - zsh: true + shell: fish + session_backend: zellij + zsh: false share_sessions: false packages: [] init_commands: [] @@ -79,7 +81,9 @@ schema: allow_agent_forwarding: bool allow_remote_port_forwarding: bool allow_tcp_forwarding: bool - zsh: bool + shell: list(fish|zsh|bash) + session_backend: list(zellij|tmux) + zsh: bool? share_sessions: bool packages: - str From e5947e1f981388e17e6ca6a77e6749204e4aac33 Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 25 Jun 2026 15:59:38 +0200 Subject: [PATCH 2/8] Install fish and zellij in the image --- ssh/Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ssh/Dockerfile b/ssh/Dockerfile index b7acbd3d..e38b6bf2 100644 --- a/ssh/Dockerfile +++ b/ssh/Dockerfile @@ -42,6 +42,7 @@ RUN \ colordiff=1.0.22-r0 \ docker-bash-completion=29.5.3-r0 \ docker-zsh-completion=29.5.3-r0 \ + fish \ gcompat=1.1.0-r4 \ git=2.54.0-r0 \ htop=3.5.1-r1 \ @@ -66,7 +67,7 @@ RUN \ nmap-ncat=7.99-r0 \ openssh=10.3_p1-r0 \ openssl=3.5.7-r0 \ - procps-ng=4.0.6-r0 \ + procps-ng=4.0.6-r1 \ pwgen=2.08-r3 \ pulseaudio-utils=17.0-r7 \ py3-pip=26.1.2-r0 \ @@ -80,6 +81,7 @@ RUN \ tmux=3.6b-r0 \ ttyd=1.7.7-r0 \ wget=1.25.0-r3 \ + zellij \ zip=3.0-r13 \ zsh-autosuggestions=0.7.1-r0 \ zsh-syntax-highlighting=0.8.0-r1 \ @@ -93,8 +95,10 @@ RUN \ \ && chmod a+x /usr/bin/ha \ && ha completion bash > /usr/share/bash-completion/completions/ha \ + && mkdir -p /usr/share/fish/vendor_completions.d \ + && ha completion fish > /usr/share/fish/vendor_completions.d/ha.fish \ \ - && sed -i -e "s#bin/sh#bin/zsh#" /etc/passwd \ + && sed -i -e "s#/bin/sh#/usr/bin/fish#" /etc/passwd \ \ && cp /usr/bin/docker /usr/local/bin/.undocked \ \ From df00a22b2bf0dbef84f734e9d1812ace9baf7b96 Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 25 Jun 2026 15:59:50 +0200 Subject: [PATCH 3/8] Use selected shell and session backend in web terminal --- ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run index abd96cb4..4b3b42e2 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run @@ -7,6 +7,44 @@ declare ttyd_command declare -a options declare ingress_port +declare login_shell +declare session_backend +declare shell_path + +get_login_shell() { + if bashio::config.has_value 'shell'; then + bashio::config 'shell' + elif bashio::config.true 'zsh'; then + echo 'zsh' + else + echo 'bash' + fi +} + +get_shell_path() { + case "$1" in + fish) + echo '/usr/bin/fish' + ;; + zsh) + echo '/bin/zsh' + ;; + bash) + echo '/bin/bash' + ;; + *) + bashio::exit.nok "Unsupported shell: $1" + ;; + esac +} + +get_session_backend() { + if bashio::config.has_value 'session_backend'; then + bashio::config 'session_backend' + else + echo 'zellij' + fi +} bashio::log.info 'Starting the ttyd daemon...' @@ -27,10 +65,21 @@ options+=(--writable) ingress_port=$(bashio::app.ingress_port) options+=(-p "${ingress_port}") -ttyd_command=(tmux -u new -A -s homeassistant zsh -l) -if ! bashio::config.true "zsh"; then - ttyd_command=(tmux -u new -A -s homeassistant bash -l) -fi +login_shell=$(get_login_shell) +shell_path=$(get_shell_path "${login_shell}") +session_backend=$(get_session_backend) + +case "${session_backend}" in + zellij) + ttyd_command=(zellij attach --create homeassistant options --default-shell "${shell_path}") + ;; + tmux) + ttyd_command=(tmux -u new -A -s homeassistant "${shell_path}" -l) + ;; + *) + bashio::exit.nok "Unsupported session backend: ${session_backend}" + ;; +esac # Change working directory cd /root || bashio::exit.nok 'Unable to change working directory' From ff4cda3663e65745e617367886493781d5e6726e Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 25 Jun 2026 16:00:11 +0200 Subject: [PATCH 4/8] Use a neutral wrapper shell for the SSH login user --- .../etc/s6-overlay/s6-rc.d/init-ssh/run | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run index b6fa82fc..01ed84e9 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run @@ -98,8 +98,9 @@ username=$(bashio::string.lower "${username}") # Create user account if the user isn't root if [[ "${username}" != "root" ]]; then - # Create an user account - adduser -D "${username}" -s "/bin/zsh" \ + # Create an user account. Keep the SSH login user on /bin/sh so it can + # consistently hand off to the configured root login shell via sudo -i. + adduser -D "${username}" -s "/bin/sh" \ || bashio::exit.nok 'Failed creating the user account' # Add new user to the wheel group @@ -107,7 +108,7 @@ if [[ "${username}" != "root" ]]; then || bashio::exit.nok 'Failed adding user to wheel group' # Ensure new user switches to root after login - echo 'exec sudo -i' > "/home/${username}/.zprofile" \ + echo 'exec sudo -i' > "/home/${username}/.profile" \ || bashio::exit.nok 'Failed configuring user profile' fi @@ -129,7 +130,7 @@ if bashio::config.has_value 'ssh.authorized_keys'; then fi # Port -sed -i "s/Port\\ .*/Port\\ ${port}/" "${SSH_CONFIG_PATH}" \ +sed -i "s/Port\ .*/Port\ ${port}/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed configuring port' # SFTP access @@ -140,16 +141,16 @@ fi # Allow specified user to log in if [[ "${username}" != "root" ]]; then - sed -i "s/AllowUsers\\ .*/AllowUsers\\ ${username}/" "${SSH_CONFIG_PATH}" \ + sed -i "s/AllowUsers\ .*/AllowUsers\ ${username}/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed opening SSH for the configured user' else - sed -i "s/PermitRootLogin\\ .*/PermitRootLogin\\ yes/" "${SSH_CONFIG_PATH}" \ + sed -i "s/PermitRootLogin\ .*/PermitRootLogin\ yes/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed opening SSH for the root user' fi # Enable password authentication when password is set if bashio::config.has_value 'ssh.password'; then - sed -i "s/PasswordAuthentication.*/PasswordAuthentication\\ yes/" \ + sed -i "s/PasswordAuthentication.*/PasswordAuthentication\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH password authentication' fi @@ -166,21 +167,21 @@ fi # Enable Agent forwarding if bashio::config.true 'ssh.allow_agent_forwarding'; then - sed -i "s/AllowAgentForwarding.*/AllowAgentForwarding\\ yes/" \ + sed -i "s/AllowAgentForwarding.*/AllowAgentForwarding\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH Agent Forwarding' fi # Allow remote port forwarding if bashio::config.true 'ssh.allow_remote_port_forwarding'; then - sed -i "s/GatewayPorts.*/GatewayPorts\\ yes/" \ + sed -i "s/GatewayPorts.*/GatewayPorts\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup remote port forwarding' fi # Allow TCP forewarding if bashio::config.true 'ssh.allow_tcp_forwarding'; then - sed -i "s/AllowTcpForwarding.*/AllowTcpForwarding\\ yes/" \ + sed -i "s/AllowTcpForwarding.*/AllowTcpForwarding\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH TCP Forwarding' fi From e0c8aef1f8f529e61c972d3913dbbb1cd0886e96 Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 25 Jun 2026 16:00:36 +0200 Subject: [PATCH 5/8] Configure fish and zellij session startup --- .../etc/s6-overlay/s6-rc.d/init-user/run | 188 +++++++++++++++--- 1 file changed, 158 insertions(+), 30 deletions(-) diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run index 113b8e86..3283a5bb 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run @@ -7,6 +7,10 @@ readonly -a DIRECTORIES=(addon_configs addons backup homeassistant media share ssl) readonly BASH_HISTORY_FILE=/root/.bash_history readonly BASH_HISTORY_PERSISTENT_FILE=/data/.bash_history +readonly FISH_CONF_D_PATH=/root/.config/fish/conf.d +readonly FISH_HOME_ASSISTANT_CONF_D_FILE=/root/.config/fish/conf.d/homeassistant.fish +readonly FISH_HISTORY_FILE=/root/.local/share/fish/fish_history +readonly FISH_HISTORY_PERSISTENT_FILE=/data/fish_history readonly GIT_CONFIG=/data/.gitconfig readonly HOME_ASSISTANT_PROFILE_D_FILE=/etc/profile.d/homeassistant.sh readonly SSH_USER_PATH=/data/.ssh @@ -14,6 +18,104 @@ readonly VSCODE_SERVER_PATH=/root/.vscode-server readonly VSCODE_SERVER_PERSISTENT_PATH=/data/.vscode-server readonly ZSH_HISTORY_FILE=/root/.zsh_history readonly ZSH_HISTORY_PERSISTENT_FILE=/data/.zsh_history +declare login_shell +declare session_backend +declare shell_path + +get_login_shell() { + if bashio::config.has_value 'shell'; then + bashio::config 'shell' + elif bashio::config.true 'zsh'; then + echo 'zsh' + else + echo 'bash' + fi +} + +get_shell_path() { + case "$1" in + fish) + echo '/usr/bin/fish' + ;; + zsh) + echo '/bin/zsh' + ;; + bash) + echo '/bin/bash' + ;; + *) + bashio::exit.nok "Unsupported shell: $1" + ;; + esac +} + +get_session_backend() { + if bashio::config.has_value 'session_backend'; then + bashio::config 'session_backend' + else + echo 'zellij' + fi +} + +set_root_shell() { + sed -i -r -e "s|^(root:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:).*|\1$1|" /etc/passwd* \ + || bashio::exit.nok 'Failed setting the root login shell' +} + +write_bash_profile() { + cat > /root/.bash_profile < /root/.zprofile < "${FISH_HOME_ASSISTANT_CONF_D_FILE}" <> "${FISH_HOME_ASSISTANT_CONF_D_FILE}" <> "${HOME_ASSISTANT_PROFILE_D_FILE}" \ @@ -88,17 +213,20 @@ if ! bashio::fs.directory_exists "${VSCODE_SERVER_PERSISTENT_PATH}"; then fi ln -s "${VSCODE_SERVER_PERSISTENT_PATH}" "${VSCODE_SERVER_PATH}" -# Disable SSH & Web Terminal session sharing if configured -if ! bashio::config.true 'share_sessions'; then +# Configure SSH & Web Terminal session sharing if requested. +if bashio::config.true 'share_sessions'; then + write_bash_profile + write_zsh_profile +else bashio::log.notice 'Session sharing has been disabled!' - rm /root/.bash_profile - rm /root/.zprofile + rm -f /root/.bash_profile + rm -f /root/.zprofile fi # Install user configured/requested packages # # Failures here are intentionally non-fatal: if the package indexes or a -# package cannot be fetched (e.g. broken DNS or no network), we still want +# package cannot be fetched (e.g., broken DNS or no network), we still want # the terminal to come up so the host remains reachable for debugging. if bashio::config.has_value 'packages'; then if apk update; then @@ -119,11 +247,11 @@ if bashio::config.has_value 'init_commands'; then # Use bashio::config to properly iterate over the array, preserving multi-line commands length=$(bashio::config 'init_commands | length') \ || bashio::exit.nok 'Failed to get init_commands array length' - + for (( i=0; i Date: Tue, 21 Jul 2026 16:10:39 +0200 Subject: [PATCH 6/8] Fix: Pin fish and zellij package versions and remove hardcoded shell default - Pin fish to 3.7.1-r0 (latest in Alpine 3.21) - Pin zellij to 0.40.0-r0 (latest in Alpine 3.21) - Remove hardcoded sed that changes /bin/sh to /usr/bin/fish globally This allows startup scripts to handle shell selection via config The shell selection should be determined at runtime by the startup scripts based on the 'shell' and legacy 'zsh' configuration options, not hardcoded in the image. --- ssh/Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ssh/Dockerfile b/ssh/Dockerfile index e38b6bf2..c1383bfe 100644 --- a/ssh/Dockerfile +++ b/ssh/Dockerfile @@ -42,7 +42,7 @@ RUN \ colordiff=1.0.22-r0 \ docker-bash-completion=29.5.3-r0 \ docker-zsh-completion=29.5.3-r0 \ - fish \ + fish=3.7.1-r0 \ gcompat=1.1.0-r4 \ git=2.54.0-r0 \ htop=3.5.1-r1 \ @@ -81,7 +81,7 @@ RUN \ tmux=3.6b-r0 \ ttyd=1.7.7-r0 \ wget=1.25.0-r3 \ - zellij \ + zellij=0.40.0-r0 \ zip=3.0-r13 \ zsh-autosuggestions=0.7.1-r0 \ zsh-syntax-highlighting=0.8.0-r1 \ @@ -98,8 +98,6 @@ RUN \ && mkdir -p /usr/share/fish/vendor_completions.d \ && ha completion fish > /usr/share/fish/vendor_completions.d/ha.fish \ \ - && sed -i -e "s#/bin/sh#/usr/bin/fish#" /etc/passwd \ - \ && cp /usr/bin/docker /usr/local/bin/.undocked \ \ && ln -s -f /usr/bin/nvim /usr/bin/vim \ From e610882219f573abe2d4a7396e8993d129bdb344 Mon Sep 17 00:00:00 2001 From: "Nick B." Date: Thu, 6 Aug 2026 15:13:14 +0200 Subject: [PATCH 7/8] Fix shell and session backend compatibility Preserve legacy defaults unless the new selectors are explicitly set, keep the root account POSIX-compatible for remote commands, and dispatch only interactive sessions through shell/backend wrappers. Add regression coverage, shell/YAML validation, and updated documentation. --- .github/workflows/ci.yaml | 9 ++ README.md | 9 +- ssh/DOCS.md | 74 +++++---- ssh/Dockerfile | 2 +- ssh/config.yaml | 10 +- .../etc/s6-overlay/s6-rc.d/init-ssh/run | 25 ++-- .../etc/s6-overlay/s6-rc.d/init-user/run | 141 +++++------------- ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run | 56 +------ ssh/rootfs/root/.bash_profile | 18 ++- ssh/rootfs/root/.zprofile | 5 +- ssh/rootfs/usr/local/bin/ssh-login | 12 ++ ssh/rootfs/usr/local/bin/terminal-session | 24 +++ ssh/rootfs/usr/local/bin/terminal-shell | 11 ++ ssh/rootfs/usr/local/lib/terminal-config.sh | 48 ++++++ ssh/tests/test-terminal-backends.sh | 138 +++++++++++++++++ 15 files changed, 361 insertions(+), 221 deletions(-) create mode 100755 ssh/rootfs/usr/local/bin/ssh-login create mode 100755 ssh/rootfs/usr/local/bin/terminal-session create mode 100755 ssh/rootfs/usr/local/bin/terminal-shell create mode 100644 ssh/rootfs/usr/local/lib/terminal-config.sh create mode 100755 ssh/tests/test-terminal-backends.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e66862be..bb853d57 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,5 +17,14 @@ permissions: security-events: write jobs: + terminal-backends: + name: Terminal backend tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Test shell and session dispatch + run: bash ssh/tests/test-terminal-backends.sh + workflows: uses: hassio-addons/workflows/.github/workflows/app-ci.yaml@383c10d83acbe341acbb35a4a61bfd14827f00f0 # v3.0.0 diff --git a/README.md b/README.md index 33561a96..87e45906 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,13 @@ well. Additionally, it comes out of the box with the following: your favorite tools, which will be available every single time you log in. - Execute custom commands on app start so that you can customize the shell to your likings. -- [ZSH][zsh] as its default shell. Easier to use for the beginner, more advanced - for the more experienced user. It even comes preloaded with - ["Oh My ZSH"][ohmyzsh], with some plugins enabled as well. +- Selectable interactive shells: Zsh with Oh My Zsh remains the compatible + default, while Fish and Bash are available through the `shell` option. +- Selectable terminal session backends: tmux remains the compatible default, + while Zellij is available through the `session_backend` option. - Contains a sensible set of tools right out of the box: curl, Wget, RSync, GIT, Nmap, Mosquitto client, MariaDB/MySQL client, Awake ("wake on LAN"), Nano, - Neovim, tmux, and a bunch commonly used networking tools. + Neovim, tmux, Zellij, and a bunch commonly used networking tools. ## Support diff --git a/ssh/DOCS.md b/ssh/DOCS.md index d0cc5b6d..d6bb679e 100644 --- a/ssh/DOCS.md +++ b/ssh/DOCS.md @@ -44,12 +44,13 @@ well. Additionally, it comes out of the box with the following: your favorite tools, which will be available every single time you log in. - Execute custom commands on app start so that you can customize the shell to your likings. -- [ZSH][zsh] as its default shell. Easier to use for the beginner, more advanced - for the more experienced user. It even comes preloaded with - ["Oh My ZSH"][ohmyzsh], with some plugins enabled as well. +- Selectable interactive shells: Zsh with Oh My Zsh remains the compatible + default, while Fish and Bash are available through the `shell` option. +- Selectable terminal session backends: tmux remains the compatible default, + while Zellij is available through the `session_backend` option. - Contains a sensible set of tools right out of the box: curl, Wget, RSync, GIT, Nmap, Mosquitto client, MariaDB/MySQL client, Awake ("wake on LAN"), Nano, - Neovim, tmux, and a bunch commonly used networking tools. + Neovim, tmux, Zellij, and a bunch commonly used networking tools. ## Installation @@ -85,7 +86,8 @@ ssh: allow_agent_forwarding: false allow_remote_port_forwarding: false allow_tcp_forwarding: false -zsh: true +shell: fish +session_backend: zellij share_sessions: true packages: - build-base @@ -197,19 +199,39 @@ Nevertheless, this warning is debatable._ The following options are shared between both the SSH and the Web Terminal. +#### Option: `shell` + +Selects the interactive shell used by SSH and the Web Terminal. Supported values +are `fish`, `zsh`, and `bash`. If this option is omitted, the legacy `zsh` +option remains authoritative so existing installations keep their current shell. + +The root account itself deliberately keeps Bash as its account shell. The +selected interactive shell is started only after login, which keeps remote SSH +commands and tools such as rsync on a POSIX-compatible command shell. + +#### Option: `session_backend` + +Selects the terminal multiplexer used by the Web Terminal and, when session +sharing is enabled, SSH. Supported values are `zellij` and `tmux`. If this +option is omitted, tmux remains the default for compatibility with existing +installations. + +Zellij uses mirrored sessions and its simplified UI in this app so simultaneous +SSH and Web Terminal clients see the same workspace without requiring special +terminal fonts. + #### Option: `zsh` -The app has ZSH pre-installed and configured as the default shell. -However, ZSH might not be your preferred choice. By setting this option to -`false`, you will disable ZSH and the app will fallback to Bash instead. +This is the legacy shell selector. It remains supported for upgrades: `true` +selects Zsh and `false` selects Bash when `shell` is absent. New +configurations should use `shell` instead. #### Option: `share_sessions` -By default, the terminal session between the web client and SSH is shared. -This allows you to pick up where you left your terminal from either of those. - -This option allows you to disable this behavior by setting it to `false`, which -effectively sets SSH to behave as it used to be. +When enabled, interactive SSH clients attach to the same multiplexer session as +the Web Terminal. When disabled, SSH starts the selected shell without attaching +to the Web Terminal session. Non-interactive SSH commands never enter a +multiplexer. #### Option: `packages` @@ -258,33 +280,21 @@ client uses the clipboard behavior of its own terminal instead. ## Known issues and limitations - When SFTP is enabled, the username MUST be set to `root`. -- If you want to use rsync for file transfer, the username MUST be set to - `root`. ## Running the `ha` command or Supervisor API non-interactively -When you log in interactively, the app starts a login shell that sets up the -`SUPERVISOR_TOKEN` environment variable. The `ha` command and the Supervisor -API need that token, so commands like `ha core info` just work. - -Running a command non-interactively does **not** start a login shell, so the -token is not set and the command fails with a `401` error. For example, this -fails: +Non-interactive SSH commands run through root's Bash login environment, even +when Fish or Zsh is selected for interactive use. This keeps shell scripting +compatible and makes the `SUPERVISOR_TOKEN` available without entering the +shared terminal session: ```shell ssh your-instance "ha core info" ``` -Wrap the command in a login shell so the environment, and with it the token, -is loaded: - -```shell -ssh your-instance 'bash -lc "ha core info"' -``` - -The same applies when calling the Supervisor API directly or running commands -from automations: invoke them through a login shell (`bash -lc '...'`) so the -`SUPERVISOR_TOKEN` is available. +The command's output and exit status are returned directly to the SSH client. +Interactive logins still use the configured `shell` and, when enabled, the +configured shared-session backend. ## Changelog & Releases diff --git a/ssh/Dockerfile b/ssh/Dockerfile index c1383bfe..65ac4d30 100644 --- a/ssh/Dockerfile +++ b/ssh/Dockerfile @@ -67,7 +67,7 @@ RUN \ nmap-ncat=7.99-r0 \ openssh=10.3_p1-r0 \ openssl=3.5.7-r0 \ - procps-ng=4.0.6-r1 \ + procps-ng=4.0.6-r0 \ pwgen=2.08-r3 \ pulseaudio-utils=17.0-r7 \ py3-pip=26.1.2-r0 \ diff --git a/ssh/config.yaml b/ssh/config.yaml index 9bf6fd2f..a1fb3f78 100644 --- a/ssh/config.yaml +++ b/ssh/config.yaml @@ -63,9 +63,7 @@ options: allow_agent_forwarding: false allow_remote_port_forwarding: false allow_tcp_forwarding: false - shell: fish - session_backend: zellij - zsh: false + zsh: true share_sessions: false packages: [] init_commands: [] @@ -81,9 +79,9 @@ schema: allow_agent_forwarding: bool allow_remote_port_forwarding: bool allow_tcp_forwarding: bool - shell: list(fish|zsh|bash) - session_backend: list(zellij|tmux) - zsh: bool? + shell: list(fish|zsh|bash)? + session_backend: list(zellij|tmux)? + zsh: bool share_sessions: bool packages: - str diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run index 01ed84e9..c7857524 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run @@ -8,6 +8,7 @@ readonly SSH_AUTHORIZED_KEYS_PATH=/etc/ssh/authorized_keys readonly SSH_CONFIG_PATH=/etc/ssh/sshd_config readonly SSH_HOST_ED25519_KEY=/data/ssh_host_ed25519_key readonly SSH_HOST_RSA_KEY=/data/ssh_host_rsa_key +readonly SSH_LOGIN_SHELL=/usr/local/bin/ssh-login declare password declare port declare username @@ -98,18 +99,14 @@ username=$(bashio::string.lower "${username}") # Create user account if the user isn't root if [[ "${username}" != "root" ]]; then - # Create an user account. Keep the SSH login user on /bin/sh so it can - # consistently hand off to the configured root login shell via sudo -i. - adduser -D "${username}" -s "/bin/sh" \ + # The wrapper elevates both interactive sessions and remote commands while + # keeping non-interactive commands on root's POSIX-compatible Bash shell. + adduser -D "${username}" -s "${SSH_LOGIN_SHELL}" \ || bashio::exit.nok 'Failed creating the user account' # Add new user to the wheel group adduser "${username}" wheel \ || bashio::exit.nok 'Failed adding user to wheel group' - - # Ensure new user switches to root after login - echo 'exec sudo -i' > "/home/${username}/.profile" \ - || bashio::exit.nok 'Failed configuring user profile' fi # We need to set a password for the user account @@ -130,7 +127,7 @@ if bashio::config.has_value 'ssh.authorized_keys'; then fi # Port -sed -i "s/Port\ .*/Port\ ${port}/" "${SSH_CONFIG_PATH}" \ +sed -i "s/Port\\ .*/Port\\ ${port}/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed configuring port' # SFTP access @@ -141,16 +138,16 @@ fi # Allow specified user to log in if [[ "${username}" != "root" ]]; then - sed -i "s/AllowUsers\ .*/AllowUsers\ ${username}/" "${SSH_CONFIG_PATH}" \ + sed -i "s/AllowUsers\\ .*/AllowUsers\\ ${username}/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed opening SSH for the configured user' else - sed -i "s/PermitRootLogin\ .*/PermitRootLogin\ yes/" "${SSH_CONFIG_PATH}" \ + sed -i "s/PermitRootLogin\\ .*/PermitRootLogin\\ yes/" "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed opening SSH for the root user' fi # Enable password authentication when password is set if bashio::config.has_value 'ssh.password'; then - sed -i "s/PasswordAuthentication.*/PasswordAuthentication\ yes/" \ + sed -i "s/PasswordAuthentication.*/PasswordAuthentication\\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH password authentication' fi @@ -167,21 +164,21 @@ fi # Enable Agent forwarding if bashio::config.true 'ssh.allow_agent_forwarding'; then - sed -i "s/AllowAgentForwarding.*/AllowAgentForwarding\ yes/" \ + sed -i "s/AllowAgentForwarding.*/AllowAgentForwarding\\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH Agent Forwarding' fi # Allow remote port forwarding if bashio::config.true 'ssh.allow_remote_port_forwarding'; then - sed -i "s/GatewayPorts.*/GatewayPorts\ yes/" \ + sed -i "s/GatewayPorts.*/GatewayPorts\\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup remote port forwarding' fi # Allow TCP forewarding if bashio::config.true 'ssh.allow_tcp_forwarding'; then - sed -i "s/AllowTcpForwarding.*/AllowTcpForwarding\ yes/" \ + sed -i "s/AllowTcpForwarding.*/AllowTcpForwarding\\ yes/" \ "${SSH_CONFIG_PATH}" \ || bashio::exit.nok 'Failed to setup SSH TCP Forwarding' fi diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run index 3283a5bb..bfc9c9f3 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run @@ -7,114 +7,45 @@ readonly -a DIRECTORIES=(addon_configs addons backup homeassistant media share ssl) readonly BASH_HISTORY_FILE=/root/.bash_history readonly BASH_HISTORY_PERSISTENT_FILE=/data/.bash_history -readonly FISH_CONF_D_PATH=/root/.config/fish/conf.d -readonly FISH_HOME_ASSISTANT_CONF_D_FILE=/root/.config/fish/conf.d/homeassistant.fish readonly FISH_HISTORY_FILE=/root/.local/share/fish/fish_history readonly FISH_HISTORY_PERSISTENT_FILE=/data/fish_history readonly GIT_CONFIG=/data/.gitconfig readonly HOME_ASSISTANT_PROFILE_D_FILE=/etc/profile.d/homeassistant.sh readonly SSH_USER_PATH=/data/.ssh +readonly TERMINAL_CONFIG_FILE=/etc/terminal-session.conf +readonly TERMINAL_SHELL_COMMAND=/usr/local/bin/terminal-shell readonly VSCODE_SERVER_PATH=/root/.vscode-server readonly VSCODE_SERVER_PERSISTENT_PATH=/data/.vscode-server readonly ZSH_HISTORY_FILE=/root/.zsh_history readonly ZSH_HISTORY_PERSISTENT_FILE=/data/.zsh_history declare login_shell declare session_backend +declare share_sessions=false declare shell_path -get_login_shell() { - if bashio::config.has_value 'shell'; then - bashio::config 'shell' - elif bashio::config.true 'zsh'; then - echo 'zsh' - else - echo 'bash' - fi -} - -get_shell_path() { - case "$1" in - fish) - echo '/usr/bin/fish' - ;; - zsh) - echo '/bin/zsh' - ;; - bash) - echo '/bin/bash' - ;; - *) - bashio::exit.nok "Unsupported shell: $1" - ;; - esac -} - -get_session_backend() { - if bashio::config.has_value 'session_backend'; then - bashio::config 'session_backend' - else - echo 'zellij' - fi -} - -set_root_shell() { - sed -i -r -e "s|^(root:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:).*|\1$1|" /etc/passwd* \ - || bashio::exit.nok 'Failed setting the root login shell' -} - -write_bash_profile() { - cat > /root/.bash_profile < /root/.zprofile < "${FISH_HOME_ASSISTANT_CONF_D_FILE}" < "${TERMINAL_CONFIG_FILE}" <> "${FISH_HOME_ASSISTANT_CONF_D_FILE}" <> "${HOME_ASSISTANT_PROFILE_D_FILE}" \ @@ -213,14 +148,8 @@ if ! bashio::fs.directory_exists "${VSCODE_SERVER_PERSISTENT_PATH}"; then fi ln -s "${VSCODE_SERVER_PERSISTENT_PATH}" "${VSCODE_SERVER_PATH}" -# Configure SSH & Web Terminal session sharing if requested. -if bashio::config.true 'share_sessions'; then - write_bash_profile - write_zsh_profile -else +if [[ "${share_sessions}" == 'false' ]]; then bashio::log.notice 'Session sharing has been disabled!' - rm -f /root/.bash_profile - rm -f /root/.zprofile fi # Install user configured/requested packages diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run index 4b3b42e2..d5de2c0d 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run @@ -7,44 +7,6 @@ declare ttyd_command declare -a options declare ingress_port -declare login_shell -declare session_backend -declare shell_path - -get_login_shell() { - if bashio::config.has_value 'shell'; then - bashio::config 'shell' - elif bashio::config.true 'zsh'; then - echo 'zsh' - else - echo 'bash' - fi -} - -get_shell_path() { - case "$1" in - fish) - echo '/usr/bin/fish' - ;; - zsh) - echo '/bin/zsh' - ;; - bash) - echo '/bin/bash' - ;; - *) - bashio::exit.nok "Unsupported shell: $1" - ;; - esac -} - -get_session_backend() { - if bashio::config.has_value 'session_backend'; then - bashio::config 'session_backend' - else - echo 'zellij' - fi -} bashio::log.info 'Starting the ttyd daemon...' @@ -65,21 +27,9 @@ options+=(--writable) ingress_port=$(bashio::app.ingress_port) options+=(-p "${ingress_port}") -login_shell=$(get_login_shell) -shell_path=$(get_shell_path "${login_shell}") -session_backend=$(get_session_backend) - -case "${session_backend}" in - zellij) - ttyd_command=(zellij attach --create homeassistant options --default-shell "${shell_path}") - ;; - tmux) - ttyd_command=(tmux -u new -A -s homeassistant "${shell_path}" -l) - ;; - *) - bashio::exit.nok "Unsupported session backend: ${session_backend}" - ;; -esac +# Web Terminal clients always use the selected backend. share_sessions controls +# whether interactive SSH clients attach to this same backend session. +ttyd_command=(/usr/local/bin/terminal-session) # Change working directory cd /root || bashio::exit.nok 'Unable to change working directory' diff --git a/ssh/rootfs/root/.bash_profile b/ssh/rootfs/root/.bash_profile index 718237b1..3e4385e2 100644 --- a/ssh/rootfs/root/.bash_profile +++ b/ssh/rootfs/root/.bash_profile @@ -1,3 +1,17 @@ -if [[ -z "$TMUX" ]]; then - exec tmux -u new -A -s homeassistant bash -l +# shellcheck shell=bash +# Interactive shell selection and session sharing are configured at startup. +# Non-interactive login shells must never be redirected into a multiplexer. +if [[ $- == *i* ]] \ + && [[ -z "${APP_SELECTED_SHELL:-}" ]] \ + && [[ -z "${TMUX:-}" ]] \ + && [[ -z "${ZELLIJ:-}" ]] \ + && [[ -r /etc/terminal-session.conf ]]; then + # shellcheck disable=SC1091 + source /etc/terminal-session.conf + + if [[ "${TERMINAL_SHARE_SESSIONS}" == 'true' ]]; then + exec /usr/local/bin/terminal-session + else + exec /usr/local/bin/terminal-shell + fi fi diff --git a/ssh/rootfs/root/.zprofile b/ssh/rootfs/root/.zprofile index 6a52f3c6..55d2db6b 100644 --- a/ssh/rootfs/root/.zprofile +++ b/ssh/rootfs/root/.zprofile @@ -1,3 +1,2 @@ -if [[ -z "$TMUX" ]] && [[ -o interactive ]]; then - exec tmux -u new -A -s homeassistant zsh -l -fi +# Root keeps Bash as its account shell. Interactive Zsh is launched through +# /usr/local/bin/terminal-shell after the login environment has been prepared. diff --git a/ssh/rootfs/usr/local/bin/ssh-login b/ssh/rootfs/usr/local/bin/ssh-login new file mode 100755 index 00000000..c8279e77 --- /dev/null +++ b/ssh/rootfs/usr/local/bin/ssh-login @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +# OpenSSH invokes the configured account shell with `-c command` for remote +# commands. Run those commands through root's POSIX login shell so scripting, +# rsync, Mosh and similar clients do not inherit the selected interactive shell. +if [ "${1:-}" = '-c' ]; then + shift + exec sudo -H /bin/bash -lc "${1:-}" +fi + +exec sudo -i diff --git a/ssh/rootfs/usr/local/bin/terminal-session b/ssh/rootfs/usr/local/bin/terminal-session new file mode 100755 index 00000000..8a9dd705 --- /dev/null +++ b/ssh/rootfs/usr/local/bin/terminal-session @@ -0,0 +1,24 @@ +#!/bin/sh +set -eu + +config_path="${TERMINAL_CONFIG_PATH:-/etc/terminal-session.conf}" +# shellcheck disable=SC1090 +. "${config_path}" + +shell_command="${TERMINAL_SHELL_COMMAND:-/usr/local/bin/terminal-shell}" + +case "${TERMINAL_SESSION_BACKEND}" in + zellij) + exec zellij attach --create homeassistant options \ + --default-shell "${shell_command}" \ + --mirror-session true \ + --simplified-ui true + ;; + tmux) + exec tmux -u new -A -s homeassistant "${shell_command}" + ;; + *) + echo "Unsupported session backend: ${TERMINAL_SESSION_BACKEND}" >&2 + exit 1 + ;; +esac diff --git a/ssh/rootfs/usr/local/bin/terminal-shell b/ssh/rootfs/usr/local/bin/terminal-shell new file mode 100755 index 00000000..542652a9 --- /dev/null +++ b/ssh/rootfs/usr/local/bin/terminal-shell @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +config_path="${TERMINAL_CONFIG_PATH:-/etc/terminal-session.conf}" +# shellcheck disable=SC1090 +. "${config_path}" + +export APP_SELECTED_SHELL=1 +export SHELL="${TERMINAL_SHELL_PATH}" + +exec "${TERMINAL_SHELL_PATH}" -l diff --git a/ssh/rootfs/usr/local/lib/terminal-config.sh b/ssh/rootfs/usr/local/lib/terminal-config.sh new file mode 100644 index 00000000..28305ceb --- /dev/null +++ b/ssh/rootfs/usr/local/lib/terminal-config.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# shellcheck shell=bash + +terminal::shell_name() { + if bashio::config.has_value 'shell'; then + bashio::config 'shell' + elif bashio::config.true 'zsh'; then + echo 'zsh' + else + echo 'bash' + fi +} + +terminal::shell_path() { + case "$1" in + fish) + echo '/usr/bin/fish' + ;; + zsh) + echo '/bin/zsh' + ;; + bash) + echo '/bin/bash' + ;; + *) + bashio::exit.nok "Unsupported shell: $1" + ;; + esac +} + +terminal::session_backend() { + local backend + + if bashio::config.has_value 'session_backend'; then + backend=$(bashio::config 'session_backend') + else + backend='tmux' + fi + + case "${backend}" in + zellij|tmux) + echo "${backend}" + ;; + *) + bashio::exit.nok "Unsupported session backend: ${backend}" + ;; + esac +} diff --git a/ssh/tests/test-terminal-backends.sh b/ssh/tests/test-terminal-backends.sh new file mode 100755 index 00000000..1752e118 --- /dev/null +++ b/ssh/tests/test-terminal-backends.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +terminal_lib="${repo_root}/ssh/rootfs/usr/local/lib/terminal-config.sh" +terminal_shell="${repo_root}/ssh/rootfs/usr/local/bin/terminal-shell" +terminal_session="${repo_root}/ssh/rootfs/usr/local/bin/terminal-session" +ssh_login="${repo_root}/ssh/rootfs/usr/local/bin/ssh-login" +bash_profile="${repo_root}/ssh/rootfs/root/.bash_profile" +declare -A config=() + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_eq() { + local expected=$1 + local actual=$2 + + [[ "${actual}" == "${expected}" ]] \ + || fail "expected '${expected}', got '${actual}'" +} + +bashio::config.has_value() { + [[ -n "${config[$1]+set}" && -n "${config[$1]}" ]] +} + +bashio::config.true() { + [[ "${config[$1]-}" == 'true' ]] +} + +bashio::config() { + printf '%s\n' "${config[$1]}" +} + +bashio::exit.nok() { + echo "$*" >&2 + return 1 +} + +# shellcheck disable=SC1090 +source "${terminal_lib}" + +# Existing installations retain their zsh/tmux or bash/tmux behaviour. +config=([zsh]=true) +assert_eq zsh "$(terminal::shell_name)" +assert_eq tmux "$(terminal::session_backend)" + +config=([zsh]=false) +assert_eq bash "$(terminal::shell_name)" +assert_eq tmux "$(terminal::session_backend)" + +# New selectors override the legacy zsh switch only when explicitly configured. +config=([zsh]=true [shell]=fish [session_backend]=zellij) +assert_eq fish "$(terminal::shell_name)" +assert_eq /usr/bin/fish "$(terminal::shell_path fish)" +assert_eq zellij "$(terminal::session_backend)" + +config=([zsh]=false [shell]=zsh [session_backend]=tmux) +assert_eq zsh "$(terminal::shell_name)" +assert_eq /bin/zsh "$(terminal::shell_path zsh)" +assert_eq tmux "$(terminal::session_backend)" + +if terminal::shell_path invalid; then + fail 'invalid shell was accepted' +fi +config=([session_backend]=invalid) +if terminal::session_backend; then + fail 'invalid session backend was accepted' +fi + +tmp_dir=$(mktemp -d) +trap 'rm -r -- "${tmp_dir}"' EXIT +mkdir -p "${tmp_dir}/bin" + +cat > "${tmp_dir}/bin/fake-shell" <<'EOF' +#!/bin/sh +printf '%s|%s|%s\n' "$1" "$SHELL" "$APP_SELECTED_SHELL" +EOF + +cat > "${tmp_dir}/bin/zellij" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" +EOF + +cat > "${tmp_dir}/bin/tmux" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" +EOF + +cat > "${tmp_dir}/bin/sudo" <<'EOF' +#!/bin/sh +printf '%s\n' "$*" +EOF + +chmod +x "${tmp_dir}/bin/"* + +cat > "${tmp_dir}/terminal.conf" < Date: Thu, 6 Aug 2026 15:45:16 +0200 Subject: [PATCH 8/8] Fix terminal backend CI and compatibility checks Use Alpine 3.24 package versions, pin the checkout action, test remote command quoting and exit statuses, make the root Bash rewrite directly testable, and clarify root/Mosh command-mode behavior. --- .github/workflows/ci.yaml | 4 +- ssh/DOCS.md | 16 +++++--- ssh/Dockerfile | 4 +- .../etc/s6-overlay/s6-rc.d/init-user/run | 12 +----- ssh/rootfs/usr/local/bin/ssh-login | 3 +- ssh/rootfs/usr/local/lib/terminal-config.sh | 16 ++++++++ ssh/tests/test-terminal-backends.sh | 38 +++++++++++++++++-- 7 files changed, 70 insertions(+), 23 deletions(-) mode change 100644 => 100755 ssh/rootfs/usr/local/lib/terminal-config.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb853d57..d959ce7f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Test shell and session dispatch run: bash ssh/tests/test-terminal-backends.sh diff --git a/ssh/DOCS.md b/ssh/DOCS.md index d6bb679e..6ca42141 100644 --- a/ssh/DOCS.md +++ b/ssh/DOCS.md @@ -283,18 +283,22 @@ client uses the clipboard behavior of its own terminal instead. ## Running the `ha` command or Supervisor API non-interactively -Non-interactive SSH commands run through root's Bash login environment, even -when Fish or Zsh is selected for interactive use. This keeps shell scripting -compatible and makes the `SUPERVISOR_TOKEN` available without entering the -shared terminal session: +Non-interactive SSH commands always run under Bash and never enter the selected +multiplexer. With the default non-root SSH username, the login wrapper executes +the command through root's Bash login environment. When logging in directly as +root, OpenSSH invokes root's Bash account shell and imports the +`SUPERVISOR_TOKEN` from the permitted SSH environment: ```shell ssh your-instance "ha core info" ``` The command's output and exit status are returned directly to the SSH client. -Interactive logins still use the configured `shell` and, when enabled, the -configured shared-session backend. +Interactive SSH and Web Terminal logins still use the configured `shell` and, +when enabled, the configured shared-session backend. + +Mosh bootstraps through non-interactive SSH command mode. It therefore starts +Bash and does not attach to the configured shared-session backend. ## Changelog & Releases diff --git a/ssh/Dockerfile b/ssh/Dockerfile index 65ac4d30..8b605923 100644 --- a/ssh/Dockerfile +++ b/ssh/Dockerfile @@ -42,7 +42,7 @@ RUN \ colordiff=1.0.22-r0 \ docker-bash-completion=29.5.3-r0 \ docker-zsh-completion=29.5.3-r0 \ - fish=3.7.1-r0 \ + fish=4.6.0-r1 \ gcompat=1.1.0-r4 \ git=2.54.0-r0 \ htop=3.5.1-r1 \ @@ -81,7 +81,7 @@ RUN \ tmux=3.6b-r0 \ ttyd=1.7.7-r0 \ wget=1.25.0-r3 \ - zellij=0.40.0-r0 \ + zellij=0.42.2-r0 \ zip=3.0-r13 \ zsh-autosuggestions=0.7.1-r0 \ zsh-syntax-highlighting=0.8.0-r1 \ diff --git a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run index bfc9c9f3..15468059 100755 --- a/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run +++ b/ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run @@ -26,15 +26,6 @@ declare shell_path # shellcheck disable=SC1091 source /usr/local/lib/terminal-config.sh -set_root_account_shell() { - # Keep the account shell POSIX-compatible. OpenSSH uses this shell with -c - # for remote commands; interactive shell selection happens in .bash_profile. - sed -i -r -e \ - 's|^(root:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:).*|\1/bin/bash|' \ - /etc/passwd* \ - || bashio::exit.nok 'Failed setting the root account shell' -} - write_terminal_config() { cat > "${TERMINAL_CONFIG_FILE}" < "${tmp_dir}/bin/sudo" <<'EOF' #!/bin/sh -printf '%s\n' "$*" +set -eu + +case "${1:-}" in + -H) + shift + exec "$@" + ;; + -i) + printf '%s\n' "$*" + ;; + *) + echo "unexpected sudo arguments: $*" >&2 + exit 64 + ;; +esac EOF chmod +x "${tmp_dir}/bin/"* @@ -119,11 +133,29 @@ output=$(PATH="${tmp_dir}/bin:${PATH}" \ TERMINAL_CONFIG_PATH="${tmp_dir}/terminal.conf" "${terminal_session}") assert_eq '-u new -A -s homeassistant /custom/terminal-shell' "${output}" -output=$(PATH="${tmp_dir}/bin:${PATH}" "${ssh_login}" -c 'printf ok') -assert_eq '-H /bin/bash -lc printf ok' "${output}" +output=$(PATH="${tmp_dir}/bin:${PATH}" "${ssh_login}" -c \ + 'printf "%s" "quoted command value"') +assert_eq 'quoted command value' "${output}" + +set +e +PATH="${tmp_dir}/bin:${PATH}" "${ssh_login}" -c 'exit 23' +command_status=$? +set -e +assert_eq 23 "${command_status}" + output=$(PATH="${tmp_dir}/bin:${PATH}" "${ssh_login}") assert_eq '-i' "${output}" +cat > "${tmp_dir}/passwd" <<'EOF' +root:x:0:0:root:/root:/bin/ash +daemon:x:2:2:daemon:/sbin:/sbin/nologin +EOF +terminal::set_root_account_shell "${tmp_dir}/passwd" +assert_eq 'root:x:0:0:root:/root:/bin/bash' \ + "$(sed -n '1p' "${tmp_dir}/passwd")" +assert_eq 'daemon:x:2:2:daemon:/sbin:/sbin/nologin' \ + "$(sed -n '2p' "${tmp_dir}/passwd")" + # A non-interactive login shell must not be redirected into a multiplexer. output=$(bash --noprofile --norc -c "source '${bash_profile}'; printf unaffected") assert_eq unaffected "${output}"