Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,16 @@ permissions:
security-events: write

jobs:
terminal-backends:
name: Terminal backend tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
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

workflows:
uses: hassio-addons/workflows/.github/workflows/app-ci.yaml@383c10d83acbe341acbb35a4a61bfd14827f00f0 # v3.0.0
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
76 changes: 45 additions & 31 deletions ssh/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -258,33 +280,25 @@ 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 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"
```

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 command's output and exit status are returned directly to the SSH client.
Interactive SSH and Web Terminal logins still use the configured `shell` and,
when enabled, the configured shared-session backend.

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.
Mosh bootstraps through non-interactive SSH command mode. It therefore starts
Bash and does not attach to the configured shared-session backend.

## Changelog & Releases

Expand Down
6 changes: 4 additions & 2 deletions ssh/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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=4.6.0-r1 \
gcompat=1.1.0-r4 \
git=2.54.0-r0 \
htop=3.5.1-r1 \
Expand Down Expand Up @@ -80,6 +81,7 @@ RUN \
tmux=3.6b-r0 \
ttyd=1.7.7-r0 \
wget=1.25.0-r3 \
zellij=0.42.2-r0 \
zip=3.0-r13 \
zsh-autosuggestions=0.7.1-r0 \
zsh-syntax-highlighting=0.8.0-r1 \
Expand All @@ -93,8 +95,8 @@ RUN \
\
&& chmod a+x /usr/bin/ha \
&& ha completion bash > /usr/share/bash-completion/completions/ha \
\
&& sed -i -e "s#bin/sh#bin/zsh#" /etc/passwd \
&& mkdir -p /usr/share/fish/vendor_completions.d \
&& ha completion fish > /usr/share/fish/vendor_completions.d/ha.fish \
\
&& cp /usr/bin/docker /usr/local/bin/.undocked \
\
Expand Down
2 changes: 2 additions & 0 deletions ssh/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ 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
share_sessions: bool
packages:
Expand Down
10 changes: 4 additions & 6 deletions ssh/rootfs/etc/s6-overlay/s6-rc.d/init-ssh/run
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,17 +99,14 @@ 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" \
# 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}/.zprofile" \
|| bashio::exit.nok 'Failed configuring user profile'
fi

# We need to set a password for the user account
Expand Down
115 changes: 82 additions & 33 deletions ssh/rootfs/etc/s6-overlay/s6-rc.d/init-user/run
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,37 @@
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_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

# shellcheck disable=SC1091
source /usr/local/lib/terminal-config.sh

write_terminal_config() {
cat > "${TERMINAL_CONFIG_FILE}" <<EOF
TERMINAL_SHELL_NAME=${login_shell}
TERMINAL_SHELL_PATH=${shell_path}
TERMINAL_SESSION_BACKEND=${session_backend}
TERMINAL_SHARE_SESSIONS=${share_sessions}
TERMINAL_SHELL_COMMAND=${TERMINAL_SHELL_COMMAND}
EOF

chmod 0644 "${TERMINAL_CONFIG_FILE}" \
|| bashio::exit.nok 'Failed setting terminal configuration permissions'
}

# Links some common directories to the user's home folder for convenience
for dir in "${DIRECTORIES[@]}"; do
Expand All @@ -27,34 +51,62 @@ ln -s "/homeassistant" "/config" \
ln -s "/homeassistant" "${HOME}/config" \
|| bashio::log.warning "Failed linking common directory: ${HOME}/config"

# Sets up ZSH or Bash shell history
if bashio::config.true "zsh"; then
touch "${ZSH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok 'Failed creating a persistent ZSH history file'

chmod 600 "${ZSH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok \
'Failed setting the correct permissions to the ZSH history file'

ln -s -f "${ZSH_HISTORY_PERSISTENT_FILE}" "${ZSH_HISTORY_FILE}" \
|| bashio::exit.nok 'Failed linking the persistent ZSH history file'
else
touch "${BASH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok 'Failed creating a persistent Bash history file'

chmod 600 "${BASH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok \
'Failed setting the correct permissions to the Bash history file'

ln -s -f "${BASH_HISTORY_PERSISTENT_FILE}" "${BASH_HISTORY_FILE}" \
|| bashio::exit.nok 'Failed linking the persistent Bash history file'
login_shell=$(terminal::shell_name)
shell_path=$(terminal::shell_path "${login_shell}")
session_backend=$(terminal::session_backend)
if bashio::config.true 'share_sessions'; then
share_sessions=true
fi

# Set up Bash
if ! bashio::config.true "zsh"; then
sed -i -r -e 's|^(root:.*)/bin/zsh$|\1/bin/bash|' /etc/passwd*
sed -i -e 's|/zsh$|/bash|' /root/.tmux.conf
fi
# Sets up shell history
case "${login_shell}" in
fish)
mkdir -p /root/.local/share/fish \
|| bashio::exit.nok 'Failed creating the Fish history directory'

touch "${FISH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok 'Failed creating a persistent Fish history file'

chmod 600 "${FISH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok \
'Failed setting the correct permissions to the Fish history file'

ln -s -f "${FISH_HISTORY_PERSISTENT_FILE}" "${FISH_HISTORY_FILE}" \
|| bashio::exit.nok 'Failed linking the persistent Fish history file'
;;
zsh)
touch "${ZSH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok 'Failed creating a persistent ZSH history file'

chmod 600 "${ZSH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok \
'Failed setting the correct permissions to the ZSH history file'

ln -s -f "${ZSH_HISTORY_PERSISTENT_FILE}" "${ZSH_HISTORY_FILE}" \
|| bashio::exit.nok 'Failed linking the persistent ZSH history file'
;;
bash)
touch "${BASH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok 'Failed creating a persistent Bash history file'

chmod 600 "${BASH_HISTORY_PERSISTENT_FILE}" \
|| bashio::exit.nok \
'Failed setting the correct permissions to the Bash history file'

ln -s -f "${BASH_HISTORY_PERSISTENT_FILE}" "${BASH_HISTORY_FILE}" \
|| bashio::exit.nok 'Failed linking the persistent Bash history file'
;;
esac

# Keep root on Bash for OpenSSH command-mode compatibility.
terminal::set_root_account_shell /etc/passwd*
write_terminal_config

# Make new tmux panes use the same login-shell wrapper as the initial pane.
sed -i -r -e \
"s|^(set-option -g default-shell ).*|\1${TERMINAL_SHELL_COMMAND}|" \
/root/.tmux.conf \
|| bashio::exit.nok 'Failed configuring the tmux default shell'

echo "export SUPERVISOR_TOKEN=\"${SUPERVISOR_TOKEN}\"" \
>> "${HOME_ASSISTANT_PROFILE_D_FILE}" \
Expand Down Expand Up @@ -88,17 +140,14 @@ 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
if [[ "${share_sessions}" == 'false' ]]; then
bashio::log.notice 'Session sharing has been disabled!'
rm /root/.bash_profile
rm /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
Expand All @@ -119,11 +168,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<length; i++ )); do
cmd=$(bashio::config "init_commands[${i}]") \
|| bashio::exit.nok "Failed to get init command at index ${i}"

eval "${cmd}" \
|| bashio::exit.nok "Failed executing init command: ${cmd}"
done
Expand Down
7 changes: 3 additions & 4 deletions ssh/rootfs/etc/s6-overlay/s6-rc.d/ttyd/run
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ 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
# 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'
Expand Down
Loading