From ead45004062641f013b433acc14ca6dfbeb84da0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 21:39:11 +0000 Subject: [PATCH 1/3] Add Tailscale SSH and Netbird SSH: first-time install prompt and Remote Access toggles Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZQveYdjfcrEsyazq32 --- lib/provision.sh | 4 ++ menus/addon_remote_access.sh | 121 ++++++++++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/lib/provision.sh b/lib/provision.sh index cf33cb8..9c3bd5b 100644 --- a/lib/provision.sh +++ b/lib/provision.sh @@ -349,6 +349,10 @@ provision_configure_kiosk_settings() { log_info "Configure later: Advanced -> Emergency Hotspot" fi + if [[ "${KIOSK_UNATTENDED:-0}" != "1" ]]; then + remote_ssh_first_time_setup + fi + echo echo "Virtual consoles (Ctrl+Alt+F1-F8) are ENABLED by default." if ! ask_yes_no "Keep virtual consoles enabled?" "y"; then diff --git a/menus/addon_remote_access.sh b/menus/addon_remote_access.sh index bd0827e..191eebf 100644 --- a/menus/addon_remote_access.sh +++ b/menus/addon_remote_access.sh @@ -264,6 +264,7 @@ tailscale_status() { if [[ "$(tailscale_backend_state)" == "Running" ]]; then echo "Tailscale: connected" + echo " SSH: $(tailscale_ssh_enabled && echo "enabled" || echo "disabled")" echo " Hostname: $(tailscale status --json 2>/dev/null | jq -r '.Self.HostName // "unknown"')" echo " IP: $(tailscale ip -4 2>/dev/null)" else @@ -271,10 +272,40 @@ tailscale_status() { fi } +# Tailscale SSH: tailscaled itself answers SSH on this machine's tailnet +# address, authenticated by tailnet identity - no keys to manage. Which +# users may log in as whom is set by the tailnet's ACL "ssh" rules in the +# Tailscale admin console (https://login.tailscale.com/admin/acls). +tailscale_ssh_enabled() { + [[ "$(tailscale debug prefs 2>/dev/null | jq -r '.RunSSH // false' 2>/dev/null)" == "true" ]] +} + +# Echoes "--ssh" when the user wants Tailscale SSH, nothing otherwise. +tailscale_ask_ssh_flag() { + if ask_yes_no "Enable Tailscale SSH (log in over the tailnet, no SSH keys)?" "y" >&2; then + echo "--ssh" + fi +} + +action_tailscale_toggle_ssh() { + echo + if [[ "$(tailscale_backend_state)" != "Running" ]]; then + log_error "Connect Tailscale first (Connect, above)" + return 1 + fi + if tailscale_ssh_enabled; then + ask_yes_no "Tailscale SSH is ENABLED. Disable it?" "n" || { echo "Cancelled"; return; } + sudo tailscale set --ssh=false && log_success "Tailscale SSH disabled" + else + sudo tailscale set --ssh=true && log_success "Tailscale SSH enabled" + echo "Allow it in your tailnet ACL (\"ssh\" rules): https://login.tailscale.com/admin/acls" + fi +} + tailscale_menu_builder() { if command -v tailscale &>/dev/null; then - MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Show status" "Uninstall") - MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_show_status action_tailscale_uninstall) + MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Enable/disable Tailscale SSH" "Show status" "Uninstall") + MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_toggle_ssh action_tailscale_show_status action_tailscale_uninstall) else MENU_LABELS=("Install Tailscale") MENU_HANDLERS=(action_tailscale_install) @@ -285,9 +316,13 @@ tailscale_menu() { run_menu "TAILSCALE VPN" tailscale_menu_builder tailscale_status } +# $1 = "noconfirm" skips the "Install?" question (first-time install +# has already asked). action_tailscale_install() { echo - ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; } + if [[ "${1:-}" != "noconfirm" ]]; then + ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; } + fi if ! curl -fsSL https://tailscale.com/install.sh | sh; then log_error "Tailscale installation failed" @@ -310,7 +345,10 @@ action_tailscale_install() { action_tailscale_connect_interactive() { echo - if sudo tailscale up; then + local ssh_flag + ssh_flag=$(tailscale_ask_ssh_flag) + # shellcheck disable=SC2086 # ssh_flag is empty or exactly "--ssh" + if sudo tailscale up $ssh_flag; then log_success "Tailscale connected" else log_error "Tailscale connection failed" @@ -327,7 +365,10 @@ action_tailscale_connect_authkey() { return fi - if sudo tailscale up --authkey="$authkey"; then + local ssh_flag + ssh_flag=$(tailscale_ask_ssh_flag) + # shellcheck disable=SC2086 # ssh_flag is empty or exactly "--ssh" + if sudo tailscale up --authkey="$authkey" $ssh_flag; then log_success "Tailscale connected" else log_error "Tailscale connection failed" @@ -368,16 +409,48 @@ netbird_status() { if netbird_connected; then echo "Netbird: connected" + echo " SSH: $(netbird_ssh_enabled && echo "enabled" || echo "disabled")" netbird status 2>/dev/null | grep -E "NetBird IP:|Public key:" | sed 's/^/ /' || true else echo "Netbird: installed, not connected" fi } +# Netbird's built-in SSH server listens on port 22022 on the Netbird +# interface (wt0). ufw's default-deny-incoming (lib/provision.sh) would +# block it, so open that port on wt0 only. +netbird_ssh_enabled() { + netbird status 2>/dev/null | grep -qiE '^SSH Server:.*(enabled|true)' || + sudo -n grep -qE '"ServerSSHAllowed": *true' /var/lib/netbird/default.json 2>/dev/null +} + +netbird_open_ssh_firewall() { + if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then + sudo ufw allow in on wt0 to any port 22022 proto tcp >/dev/null && + log_info "Firewall: allowed Netbird SSH (22022) on wt0 only" + fi +} + +action_netbird_toggle_ssh() { + echo + if ! netbird_connected; then + log_error "Connect Netbird first (Connect, above)" + return 1 + fi + if netbird_ssh_enabled; then + ask_yes_no "Netbird SSH is ENABLED. Disable it?" "n" || { echo "Cancelled"; return; } + sudo netbird up --allow-server-ssh=false && log_success "Netbird SSH disabled" + else + sudo netbird up --allow-server-ssh && log_success "Netbird SSH enabled" && + netbird_open_ssh_firewall + echo "Connect from another Netbird peer: ssh @ (port 22022 if your client needs it)" + fi +} + netbird_menu_builder() { if command -v netbird &>/dev/null; then - MENU_LABELS=("Connect with setup key" "Show status" "Uninstall") - MENU_HANDLERS=(action_netbird_connect action_netbird_show_status action_netbird_uninstall) + MENU_LABELS=("Connect with setup key" "Enable/disable Netbird SSH" "Show status" "Uninstall") + MENU_HANDLERS=(action_netbird_connect action_netbird_toggle_ssh action_netbird_show_status action_netbird_uninstall) else MENU_LABELS=("Install Netbird") MENU_HANDLERS=(action_netbird_install) @@ -388,9 +461,13 @@ netbird_menu() { run_menu "NETBIRD VPN" netbird_menu_builder netbird_status } +# $1 = "noconfirm" skips the "Install?" question (first-time install +# has already asked). action_netbird_install() { echo - ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; } + if [[ "${1:-}" != "noconfirm" ]]; then + ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; } + fi if ! curl -fsSL https://pkgs.netbird.io/install.sh | sh; then log_error "Netbird installation failed" @@ -414,8 +491,13 @@ action_netbird_connect() { return fi - if sudo netbird up --setup-key "$setup_key" --allow-server-ssh; then + local ssh_flag=() + if ask_yes_no "Enable Netbird SSH (log in over the Netbird network)?" "y"; then + ssh_flag=(--allow-server-ssh) + fi + if sudo netbird up --setup-key "$setup_key" "${ssh_flag[@]}"; then log_success "Netbird connected" + if [[ ${#ssh_flag[@]} -gt 0 ]]; then netbird_open_ssh_firewall; fi else log_error "Netbird connection failed" fi @@ -438,3 +520,24 @@ netbird_do_uninstall() { sudo apt remove -y netbird 2>/dev/null || true log_success "Netbird removed" } + +################################################################################ +# First-time install hook (called from lib/provision.sh): offer to set up +# one of the two mesh-VPN providers with SSH enabled, so the kiosk is +# reachable remotely right after install. +################################################################################ +remote_ssh_first_time_setup() { + echo + echo "Remote SSH access over a mesh VPN lets you manage the kiosk from" + echo "anywhere with no port forwarding:" + echo " 1. Tailscale (with Tailscale SSH)" + echo " 2. Netbird (with Netbird SSH)" + echo " 3. Skip (configure later: Addons -> Remote Access)" + local choice + choice=$(ask_integer "Choose" "3" 1 3) + case "$choice" in + 1) action_tailscale_install noconfirm ;; + 2) action_netbird_install noconfirm ;; + *) log_info "Configure later: Addons -> Remote Access" ;; + esac +} From eb7bc14a9ca00801b767ca9451e6ad351a4a8c97 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 21:40:49 +0000 Subject: [PATCH 2/3] Revert Tailscale/Netbird SSH toggles (misread request) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZQveYdjfcrEsyazq32 --- lib/provision.sh | 4 -- menus/addon_remote_access.sh | 121 +++-------------------------------- 2 files changed, 9 insertions(+), 116 deletions(-) diff --git a/lib/provision.sh b/lib/provision.sh index 9c3bd5b..cf33cb8 100644 --- a/lib/provision.sh +++ b/lib/provision.sh @@ -349,10 +349,6 @@ provision_configure_kiosk_settings() { log_info "Configure later: Advanced -> Emergency Hotspot" fi - if [[ "${KIOSK_UNATTENDED:-0}" != "1" ]]; then - remote_ssh_first_time_setup - fi - echo echo "Virtual consoles (Ctrl+Alt+F1-F8) are ENABLED by default." if ! ask_yes_no "Keep virtual consoles enabled?" "y"; then diff --git a/menus/addon_remote_access.sh b/menus/addon_remote_access.sh index 191eebf..bd0827e 100644 --- a/menus/addon_remote_access.sh +++ b/menus/addon_remote_access.sh @@ -264,7 +264,6 @@ tailscale_status() { if [[ "$(tailscale_backend_state)" == "Running" ]]; then echo "Tailscale: connected" - echo " SSH: $(tailscale_ssh_enabled && echo "enabled" || echo "disabled")" echo " Hostname: $(tailscale status --json 2>/dev/null | jq -r '.Self.HostName // "unknown"')" echo " IP: $(tailscale ip -4 2>/dev/null)" else @@ -272,40 +271,10 @@ tailscale_status() { fi } -# Tailscale SSH: tailscaled itself answers SSH on this machine's tailnet -# address, authenticated by tailnet identity - no keys to manage. Which -# users may log in as whom is set by the tailnet's ACL "ssh" rules in the -# Tailscale admin console (https://login.tailscale.com/admin/acls). -tailscale_ssh_enabled() { - [[ "$(tailscale debug prefs 2>/dev/null | jq -r '.RunSSH // false' 2>/dev/null)" == "true" ]] -} - -# Echoes "--ssh" when the user wants Tailscale SSH, nothing otherwise. -tailscale_ask_ssh_flag() { - if ask_yes_no "Enable Tailscale SSH (log in over the tailnet, no SSH keys)?" "y" >&2; then - echo "--ssh" - fi -} - -action_tailscale_toggle_ssh() { - echo - if [[ "$(tailscale_backend_state)" != "Running" ]]; then - log_error "Connect Tailscale first (Connect, above)" - return 1 - fi - if tailscale_ssh_enabled; then - ask_yes_no "Tailscale SSH is ENABLED. Disable it?" "n" || { echo "Cancelled"; return; } - sudo tailscale set --ssh=false && log_success "Tailscale SSH disabled" - else - sudo tailscale set --ssh=true && log_success "Tailscale SSH enabled" - echo "Allow it in your tailnet ACL (\"ssh\" rules): https://login.tailscale.com/admin/acls" - fi -} - tailscale_menu_builder() { if command -v tailscale &>/dev/null; then - MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Enable/disable Tailscale SSH" "Show status" "Uninstall") - MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_toggle_ssh action_tailscale_show_status action_tailscale_uninstall) + MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Show status" "Uninstall") + MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_show_status action_tailscale_uninstall) else MENU_LABELS=("Install Tailscale") MENU_HANDLERS=(action_tailscale_install) @@ -316,13 +285,9 @@ tailscale_menu() { run_menu "TAILSCALE VPN" tailscale_menu_builder tailscale_status } -# $1 = "noconfirm" skips the "Install?" question (first-time install -# has already asked). action_tailscale_install() { echo - if [[ "${1:-}" != "noconfirm" ]]; then - ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; } - fi + ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; } if ! curl -fsSL https://tailscale.com/install.sh | sh; then log_error "Tailscale installation failed" @@ -345,10 +310,7 @@ action_tailscale_install() { action_tailscale_connect_interactive() { echo - local ssh_flag - ssh_flag=$(tailscale_ask_ssh_flag) - # shellcheck disable=SC2086 # ssh_flag is empty or exactly "--ssh" - if sudo tailscale up $ssh_flag; then + if sudo tailscale up; then log_success "Tailscale connected" else log_error "Tailscale connection failed" @@ -365,10 +327,7 @@ action_tailscale_connect_authkey() { return fi - local ssh_flag - ssh_flag=$(tailscale_ask_ssh_flag) - # shellcheck disable=SC2086 # ssh_flag is empty or exactly "--ssh" - if sudo tailscale up --authkey="$authkey" $ssh_flag; then + if sudo tailscale up --authkey="$authkey"; then log_success "Tailscale connected" else log_error "Tailscale connection failed" @@ -409,48 +368,16 @@ netbird_status() { if netbird_connected; then echo "Netbird: connected" - echo " SSH: $(netbird_ssh_enabled && echo "enabled" || echo "disabled")" netbird status 2>/dev/null | grep -E "NetBird IP:|Public key:" | sed 's/^/ /' || true else echo "Netbird: installed, not connected" fi } -# Netbird's built-in SSH server listens on port 22022 on the Netbird -# interface (wt0). ufw's default-deny-incoming (lib/provision.sh) would -# block it, so open that port on wt0 only. -netbird_ssh_enabled() { - netbird status 2>/dev/null | grep -qiE '^SSH Server:.*(enabled|true)' || - sudo -n grep -qE '"ServerSSHAllowed": *true' /var/lib/netbird/default.json 2>/dev/null -} - -netbird_open_ssh_firewall() { - if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then - sudo ufw allow in on wt0 to any port 22022 proto tcp >/dev/null && - log_info "Firewall: allowed Netbird SSH (22022) on wt0 only" - fi -} - -action_netbird_toggle_ssh() { - echo - if ! netbird_connected; then - log_error "Connect Netbird first (Connect, above)" - return 1 - fi - if netbird_ssh_enabled; then - ask_yes_no "Netbird SSH is ENABLED. Disable it?" "n" || { echo "Cancelled"; return; } - sudo netbird up --allow-server-ssh=false && log_success "Netbird SSH disabled" - else - sudo netbird up --allow-server-ssh && log_success "Netbird SSH enabled" && - netbird_open_ssh_firewall - echo "Connect from another Netbird peer: ssh @ (port 22022 if your client needs it)" - fi -} - netbird_menu_builder() { if command -v netbird &>/dev/null; then - MENU_LABELS=("Connect with setup key" "Enable/disable Netbird SSH" "Show status" "Uninstall") - MENU_HANDLERS=(action_netbird_connect action_netbird_toggle_ssh action_netbird_show_status action_netbird_uninstall) + MENU_LABELS=("Connect with setup key" "Show status" "Uninstall") + MENU_HANDLERS=(action_netbird_connect action_netbird_show_status action_netbird_uninstall) else MENU_LABELS=("Install Netbird") MENU_HANDLERS=(action_netbird_install) @@ -461,13 +388,9 @@ netbird_menu() { run_menu "NETBIRD VPN" netbird_menu_builder netbird_status } -# $1 = "noconfirm" skips the "Install?" question (first-time install -# has already asked). action_netbird_install() { echo - if [[ "${1:-}" != "noconfirm" ]]; then - ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; } - fi + ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; } if ! curl -fsSL https://pkgs.netbird.io/install.sh | sh; then log_error "Netbird installation failed" @@ -491,13 +414,8 @@ action_netbird_connect() { return fi - local ssh_flag=() - if ask_yes_no "Enable Netbird SSH (log in over the Netbird network)?" "y"; then - ssh_flag=(--allow-server-ssh) - fi - if sudo netbird up --setup-key "$setup_key" "${ssh_flag[@]}"; then + if sudo netbird up --setup-key "$setup_key" --allow-server-ssh; then log_success "Netbird connected" - if [[ ${#ssh_flag[@]} -gt 0 ]]; then netbird_open_ssh_firewall; fi else log_error "Netbird connection failed" fi @@ -520,24 +438,3 @@ netbird_do_uninstall() { sudo apt remove -y netbird 2>/dev/null || true log_success "Netbird removed" } - -################################################################################ -# First-time install hook (called from lib/provision.sh): offer to set up -# one of the two mesh-VPN providers with SSH enabled, so the kiosk is -# reachable remotely right after install. -################################################################################ -remote_ssh_first_time_setup() { - echo - echo "Remote SSH access over a mesh VPN lets you manage the kiosk from" - echo "anywhere with no port forwarding:" - echo " 1. Tailscale (with Tailscale SSH)" - echo " 2. Netbird (with Netbird SSH)" - echo " 3. Skip (configure later: Addons -> Remote Access)" - local choice - choice=$(ask_integer "Choose" "3" 1 3) - case "$choice" in - 1) action_tailscale_install noconfirm ;; - 2) action_netbird_install noconfirm ;; - *) log_info "Configure later: Addons -> Remote Access" ;; - esac -} From 7cb4a661f96c9fdbdc2d1c1076bcef62fe406450 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 21:41:18 +0000 Subject: [PATCH 3/3] Add SSH Access: enable sshd and import keys from GitHub/Launchpad (ssh-import-id) Offered during first-time install and under Addons -> Remote Access. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013wi3ZQveYdjfcrEsyazq32 --- lib/provision.sh | 4 + menus/addon_remote_access.sh | 147 ++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/lib/provision.sh b/lib/provision.sh index cf33cb8..4344deb 100644 --- a/lib/provision.sh +++ b/lib/provision.sh @@ -349,6 +349,10 @@ provision_configure_kiosk_settings() { log_info "Configure later: Advanced -> Emergency Hotspot" fi + if [[ "${KIOSK_UNATTENDED:-0}" != "1" ]]; then + ssh_first_time_setup + fi + echo echo "Virtual consoles (Ctrl+Alt+F1-F8) are ENABLED by default." if ! ask_yes_no "Keep virtual consoles enabled?" "y"; then diff --git a/menus/addon_remote_access.sh b/menus/addon_remote_access.sh index bd0827e..85a94f1 100644 --- a/menus/addon_remote_access.sh +++ b/menus/addon_remote_access.sh @@ -1,7 +1,7 @@ #!/bin/bash ################################################################################ # menus/addon_remote_access.sh - "Remote Access" addon (VNC, WireGuard, -# Tailscale, Netbird). +# Tailscale, Netbird) plus SSH Access (GitHub/Launchpad key import). # # Third Addon migrated, and the biggest so far in scope (4 sub-areas). # All four genuinely mutate real system state at fixed paths this project @@ -31,6 +31,7 @@ VNC_APT_PACKAGES=(x11vnc) WIREGUARD_APT_PACKAGES=(wireguard wireguard-tools) remote_access_status() { + echo "SSH: $(ssh_status_line)" echo "VNC: $(is_service_active x11vnc && echo "running" || echo "not installed")" echo "WireGuard: $(wireguard_connected && echo "connected" || (command -v wg &>/dev/null && echo "installed, not connected" || echo "not installed"))" echo "Tailscale: $(command -v tailscale &>/dev/null && echo "installed" || echo "not installed")" @@ -38,8 +39,8 @@ remote_access_status() { } remote_access_menu_builder() { - MENU_LABELS=("VNC Remote Desktop" "WireGuard VPN" "Tailscale VPN" "Netbird VPN") - MENU_HANDLERS=(vnc_menu wireguard_menu tailscale_menu netbird_menu) + MENU_LABELS=("SSH Access (GitHub/Launchpad keys)" "VNC Remote Desktop" "WireGuard VPN" "Tailscale VPN" "Netbird VPN") + MENU_HANDLERS=(ssh_menu vnc_menu wireguard_menu tailscale_menu netbird_menu) } remote_access_menu() { @@ -438,3 +439,143 @@ netbird_do_uninstall() { sudo apt remove -y netbird 2>/dev/null || true log_success "Netbird removed" } + +################################################################################ +# SSH Access - openssh-server plus public keys imported from GitHub or +# Launchpad with ssh-import-id (the same "Import SSH identity" choice +# Ubuntu's own installer offers). Keys go to the admin user running this +# menu (~/.ssh/authorized_keys), never to the restricted `kiosk` user. +# The key sets published by those services are fetched over HTTPS and +# are public data, so no secret is handled here. +################################################################################ + +SSH_KEYS_ONLY_CONF="${SSHD_CONFIG_DIR:-/etc/ssh/sshd_config.d}/10-kiosk-keys-only.conf" +SSH_AUTH_KEYS="${SSH_AUTH_KEYS:-$HOME/.ssh/authorized_keys}" + +ssh_key_count() { + [[ -f "$SSH_AUTH_KEYS" ]] && grep -cE '^(ssh-|ecdsa-|sk-)' "$SSH_AUTH_KEYS" || echo 0 +} + +ssh_status_line() { + local state="not installed" + if is_service_active ssh || is_service_active sshd; then + state="running" + elif dpkg -s openssh-server &>/dev/null; then + state="installed, not running" + fi + local pw="password login on" + [[ -f "$SSH_KEYS_ONLY_CONF" ]] && pw="keys only" + echo "$state, $(ssh_key_count) key(s), $pw" +} + +ssh_status() { + echo "SSH: $(ssh_status_line)" + echo " Keys go to: $SSH_AUTH_KEYS" +} + +ssh_menu_builder() { + MENU_LABELS=("Enable SSH server" "Import keys from GitHub" "Import keys from Launchpad" "List authorized keys" "Require keys (disable password login)" "Allow password login again") + MENU_HANDLERS=(action_ssh_enable action_ssh_import_github action_ssh_import_launchpad action_ssh_list_keys action_ssh_keys_only action_ssh_allow_passwords) +} + +ssh_menu() { + run_menu "SSH ACCESS" ssh_menu_builder ssh_status +} + +action_ssh_enable() { + echo + sudo apt install -y openssh-server ssh-import-id || { log_error "Could not install openssh-server"; return 1; } + sudo systemctl enable --now ssh || { log_error "Could not start the SSH server"; return 1; } + # provision_configure_firewall already allows ssh; harmless if repeated. + if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then + sudo ufw allow ssh >/dev/null + fi + log_success "SSH server enabled" +} + +# $1 = "gh" or "lp", $2 = label for messages. +ssh_import_from() { + local prefix="$1" label="$2" user + echo + if ! command -v ssh-import-id &>/dev/null; then + sudo apt install -y ssh-import-id || { log_error "Could not install ssh-import-id"; return 1; } + fi + read -r -p "$label username (blank to cancel): " user + [[ -z "$user" ]] && { echo "Cancelled"; return; } + if [[ ! "$user" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + log_error "That doesn't look like a valid $label username" + return 1 + fi + if ssh-import-id "${prefix}:${user}"; then + log_success "Imported $label keys for $user ($(ssh_key_count) key(s) now authorized)" + else + log_error "Import failed - check the username and that this machine can reach $label" + return 1 + fi +} + +action_ssh_import_github() { ssh_import_from gh "GitHub"; } +action_ssh_import_launchpad() { ssh_import_from lp "Launchpad"; } + +action_ssh_list_keys() { + echo + if [[ "$(ssh_key_count)" == "0" ]]; then + echo "No authorized keys for $USER yet." + else + ssh-keygen -lf "$SSH_AUTH_KEYS" + fi +} + +action_ssh_keys_only() { + echo + if [[ "$(ssh_key_count)" == "0" ]]; then + log_error "No authorized keys yet - import some first, or you'd lock yourself out" + return 1 + fi + echo "Log in from another machine with a key BEFORE doing this; password" + echo "logins over SSH will stop working." + ask_yes_no "Disable SSH password login?" "n" || { echo "Cancelled"; return; } + printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' | + sudo install -D -m 644 /dev/stdin "$SSH_KEYS_ONLY_CONF" + if sudo sshd -t && sudo systemctl reload ssh; then + log_success "SSH now requires a key" + else + sudo rm -f "$SSH_KEYS_ONLY_CONF" + log_error "sshd rejected the config - reverted" + return 1 + fi +} + +action_ssh_allow_passwords() { + echo + sudo rm -f "$SSH_KEYS_ONLY_CONF" + sudo systemctl reload ssh 2>/dev/null || true + log_success "SSH password login allowed again" +} + +# First-time install hook (called from lib/provision.sh). +ssh_first_time_setup() { + echo + echo "SSH lets you manage the kiosk remotely. Ubuntu can import your" + echo "public keys from GitHub or Launchpad so you can log in with no password." + ask_yes_no "Enable SSH and import keys now?" "y" || { log_info "Configure later: Addons -> Remote Access -> SSH Access"; return; } + action_ssh_enable || return 0 + local choice + while true; do + echo + echo "Import keys from:" + echo " 1. GitHub" + echo " 2. Launchpad" + echo " 3. Done" + choice=$(ask_integer "Choose" "3" 1 3) + case "$choice" in + 1) action_ssh_import_github || true ;; + 2) action_ssh_import_launchpad || true ;; + *) break ;; + esac + done + if [[ "$(ssh_key_count)" != "0" ]]; then + echo "Tip: Remote Access -> SSH Access can switch SSH to key-only login" + echo "once you've confirmed a key works." + fi +}