diff --git a/scripts/mezzsh/mezzsh_connect.sh b/scripts/mezzsh/mezzsh_connect.sh new file mode 100644 index 0000000000..8cde44193f --- /dev/null +++ b/scripts/mezzsh/mezzsh_connect.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Connects to the Mezz PC +# Receives safety check results from the server, and warns +# the user if other users are currently connected remotely or using in person +# Allows user to force a connection + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +PC_NAME="thunderbots" +TAILSCALE_HOSTNAME="$PC_NAME" + +bash "$SCRIPT_DIR/utils/check_tailscale.sh" + +# Get the Tailscale IP of the Main PC +TARGET_IP=$(tailscale ip -4 $TAILSCALE_HOSTNAME) + +if [ -z "$TARGET_IP" ]; then + echo "Could not find Main PC on Tailscale. Are you logged in?" + exit 1 +fi + +# flag used to force a connection despite warnings +FORCE_FLAG="NORMAL" +SSH_TARGET="thunderbots@$TARGET_IP" + +# first, check server status (if other users are using the PC) +RESPONSE=$(SSH_CHECK_MODE=1 ssh -o SendEnv=SSH_CHECK_MODE -t $SSH_TARGET "check_status" 2>&1) + +RED='\e[1;31m' +NC='\e[0m' + +# someone is using IRL +if [[ "$RESPONSE" == *"STATUS_BUSY_LOCAL"* ]]; then + echo -e "⚠️ ${RED}WARNING${NC}: Someone is physically logged into the PC onsite." + read -p "Do you want to force the connection? (y/n): " choice + + # exits if user does not want to force + [[ "$choice" == [yY] ]] && FORCE_FLAG="FORCE" || exit 1 + +# someone is connected remotely +elif [[ "$RESPONSE" == *"STATUS_BUSY_REMOTE"* ]]; then + echo -e "⚠️ ${RED}WARNING${NC}: Other SSH users are connected:" + echo "$RESPONSE" | grep "List" + read -p "Do you want to force the connection? (y/n): " choice + + # exits if user does not want to force + [[ "$choice" == [yY] ]] && FORCE_FLAG="FORCE" || exit 1 +fi + +# if the user wanted to force the connection +if [ "$FORCE_FLAG" == "FORCE" ]; then + echo "Force connecting..." + FORCE_CONNECT=1 ssh -o SendEnv=FORCE_CONNECT -t $SSH_TARGET +else + ssh -t $SSH_TARGET +fi diff --git a/scripts/mezzsh/mezzsh_keygen.sh b/scripts/mezzsh/mezzsh_keygen.sh new file mode 100644 index 0000000000..651d2efa95 --- /dev/null +++ b/scripts/mezzsh/mezzsh_keygen.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Generates a private / public key pair for connecting via ssh +# Adds the Mezz PC's IP to the ssh config file, and sets the new key as the identity to use +# And sets the key file's permissions correctly +# Each key also has a username attached to it for easier identification + +PC_NAME="thunderbots" +ALIAS="mezzsh" + +GREEN='\e[1;32m' +NC='\e[0m' + +# get the actual current user despite running in sudo +USER_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6) + +# Check for root privileges +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root (use sudo)" + exit 1 +fi + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +bash "$SCRIPT_DIR/utils/check_tailscale.sh" + +# Get the Tailscale IP of the Main PC +TARGET_IP=$(tailscale ip -4 $PC_NAME) + +if [ -z "$TARGET_IP" ]; then + echo "Could not find Main PC on Tailscale. Are you logged in?" + exit 1 +fi + +read -p "Enter a username for yourself. Please make it somewhat recognizable, preferably just your first name: " USERNAME + +KEY_NAME="id_rsa_$ALIAS" +KEY_PATH="/.ssh/$KEY_NAME" +USER_KEY_PATH="$USER_HOME/.ssh/$KEY_NAME" + +echo -e "\n--- Generating SSH Key Pair ---" + +# username is appended as a comment to the key +ssh-keygen -t rsa -b 4096 -f "$USER_KEY_PATH" -C "$USERNAME" -N "" + +# ssh is particular about permissions on the key file +# specifically, the private key file should be owned and should only be readable by the current user +sudo chown $SUDO_USER:$SUDO_USER $USER_KEY_PATH +sudo chmod 400 $USER_KEY_PATH +sudo chmod 400 "$USER_KEY_PATH.pub" + +echo -e "\n--- Configuring SSH Alias ---" +cat <> "$USER_HOME/.ssh/config" + +Host $TARGET_IP + User $PC_NAME + IdentityFile ~$KEY_PATH + IdentitiesOnly yes + SendEnv SSH_CHECK_MODE FORCE_CONNECT +EOF + +echo -e "\n--- PUBLIC KEY ---" +cat "${KEY_PATH}.pub" +echo "--------------------------------------------" + +echo -e "${GREEN}Success!${NC} Please provide the whole public key above (at "$KEY_PATH.pub") to a software lead to finish setup\n" diff --git a/scripts/mezzsh/server/mezzsh_keystore.sh b/scripts/mezzsh/server/mezzsh_keystore.sh new file mode 100644 index 0000000000..0d08ad2826 --- /dev/null +++ b/scripts/mezzsh/server/mezzsh_keystore.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Adds a single public key to the Mezz PC's authorized_keys file +# along with other necessary config options + +# the script is run as sudo, but we want to modify the current user's authorized_keys +# defaults to the "thunderbots" user +USER_HOME=$(tmp=$(getent passwd "$SUDO_USER" | cut -d: -f6); echo "${tmp:-/home/thunderbots}") + +AUTH_KEYS="$USER_HOME/.ssh/authorized_keys" +SERVER_SCRIPT="/home/thunderbots/Software/scripts/mezzsh/server/mezzsh_server.sh" + +if [ "$#" -ne 1 ]; then + echo "Usage: sudo ./mezzsh_keystore.sh \"PUBLIC_KEY_STRING\"" + exit 1 +fi + +PUB_KEY="$1" + +echo "--- Registering New Remote User ---" + +# Add to authorized_keys with a command restriction +ENTRY="$PUB_KEY +command=\"$SERVER_SCRIPT\" +environment=\"SSH_CHECK_MODE=1 FORCE_CONNECT=0\"" + +if grep -q "$PUB_KEY" "$AUTH_KEYS"; then + echo "Error: This public key is already registered." +else + echo "$ENTRY" >> "$AUTH_KEYS" + chmod 600 "$AUTH_KEYS" + echo "Key added to authorized_keys." +fi + +echo "Registration complete." diff --git a/scripts/mezzsh/server/mezzsh_server.sh b/scripts/mezzsh/server/mezzsh_server.sh new file mode 100755 index 0000000000..57e1e1d49f --- /dev/null +++ b/scripts/mezzsh/server/mezzsh_server.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Script to intercept all incoming ssh connections to the Mezz PC +# Checks for both IRL users and any connected remote users who are currently using the PC +# and warns the new connection accordingly +# If the new connection wants to force, opens the connection +# and also warns any IRL users if present + +# The `who` command returns all logged in users (IRL and remote) +# Finds IRL users by looking for the physical monitor (tty2) +LOCAL_USER=$(who | grep -E '(tty2)') + +# Finds remote users currently connected +# for each one, finds their username, to make identifying them easier +# excludes the current user, otherwise server will always seem busy +REMOTE_USERS_LIST=$(bash /home/thunderbots/Software/scripts/mezzsh/utils/get_connected_users.sh) + +# if the client requested a check, return the user info from above +if [ "$SSH_CHECK_MODE" == "1" ]; then + # local user check takes priority + if [ ! -z "$LOCAL_USER" ]; then + echo "STATUS_BUSY_LOCAL" + exit 0 + elif [ ! -z "$REMOTE_USERS_LIST" ]; then + echo "STATUS_BUSY_REMOTE" + echo "List of Users: $REMOTE_USERS_LIST" + exit 0 + fi + exit 0 +fi + +# If the user actually wanted to connect, make sure: +# 1. no other users (IRL or remote) are using the PC +# OR +# 2. the force flag is provided +if ([ ! -z "$LOCAL_USER" ] || [ ! -z "$REMOTE_USERS_LIST" ]) && [ "$FORCE_CONNECT" != "1" ]; then + echo "The Mezz Computer is in use! Please use the connect script to see who is currently using it." + exit 1 +fi + +# Trigger the visual warning dialog if someone is using the PC IRL +bash /home/thunderbots/Software/scripts/mezzsh/utils/connection_warn.sh & + +# if we get here, start a normal shell +exec $SHELL diff --git a/scripts/mezzsh/server/mezzsh_setup.sh b/scripts/mezzsh/server/mezzsh_setup.sh new file mode 100644 index 0000000000..b36e9807f1 --- /dev/null +++ b/scripts/mezzsh/server/mezzsh_setup.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# Sets up the Mezz PC to accept new SSH connections +# Uses Tailscale as a VPN to get around connections being blocked at the router level +# Sets up Tailscale, ssh, and links the server script to intercept new connections + +SCRIPT_PATH="/home/thunderbots/Software/scripts/mezzsh/server/mezzsh_server.sh" +SERVER_SCRIPT="/usr/local/bin/mezzsh_server.sh" +TIMEOUT_SECONDS=3600 # 1 hour +SSHD_CONFIG="/etc/ssh/sshd_config" +TARGET_USER="thunderbots" + +# Check for root privileges +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root (use sudo)" + exit 1 +fi + +echo "--- Initializing On-Site PC SSH Security Setup ---" + +echo "[1/5] Installing Dependencies" +sudo apt update +sudo apt install openssh-server +sudo apt install zenity + +# Install Tailscale +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up --hostname=$TARGET_USER --operator=$USER + +echo "[2/5] Configuring SSH service to start on boot..." +systemctl enable --now ssh +systemctl start ssh + +echo "[3/5] Modifying sshd_config for custom Environment variables, and links script to perform safety checks on new connections......" + +# Backup original config +cp $SSHD_CONFIG "${SSHD_CONFIG}.bak" + +# This step uses a match block to modify these ssh settings for only 1 user +# Clean up any previous global ForceCommand we might have added +sed -i "/Match User $TARGET_USER/,/AcceptEnv SSH_CHECK_MODE FORCE_CONNECT/d" $SSHD_CONFIG + +# copies the script from repo to script location +cp $SCRIPT_PATH $SERVER_SCRIPT + +# Append the Match block to the end of the file +cat <> $SSHD_CONFIG +Match User $TARGET_USER + ForceCommand $SERVER_SCRIPT + AcceptEnv SSH_CHECK_MODE FORCE_CONNECT +EOF + +# Script has to be executable for it to be triggered on new connections +sudo chmod +x $SERVER_SCRIPT + +echo "[4/5] Setting 1-hour shell timeout..." + +USER_HOME=$(eval echo "~$TARGET_USER") +TIMEOUT_BLOCK='if [ -n "$SSH_TTY" ]; then export TMOUT='$TIMEOUT_SECONDS' && readonly TMOUT; fi' + +if [ -d "$USER_HOME" ]; then + # Remove old TMOUT lines if they exist and append new one + sed -i '/TMOUT/d' "$USER_HOME/.bashrc" + echo $TIMEOUT_BLOCK >> "$USER_HOME/.bashrc" + chown $TARGET_USER:$TARGET_USER "$USER_HOME/.bashrc" + echo "Success: SSH timeout set for $TARGET_USER." +else + echo "ERROR: Home directory for $TARGET_USER not found. Timeout not set." +fi + +echo "[5/5] Restarting SSH service to apply changes..." +systemctl restart ssh diff --git a/scripts/mezzsh/utils/check_tailscale.sh b/scripts/mezzsh/utils/check_tailscale.sh new file mode 100644 index 0000000000..974b8892df --- /dev/null +++ b/scripts/mezzsh/utils/check_tailscale.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Checks if Tailscale is installed, the daemon is up and running, and we are logged in (connected) to the VPN + +# Check if tailscale is installed +if ! command -v tailscale &> /dev/null; then + echo "Tailscale not found. Installing..." + curl -fsSL https://tailscale.com/install.sh | sh +fi + +# Check if the Tailscale daemon (tailscaled) is even running +if ! systemctl is-active --quiet tailscaled; then + echo "tailscaled is not running. Starting service..." + sudo systemctl start tailscaled +fi + +# Check if the node is authenticated and connected to the tailnet +# 'tailscale status' returns 0 if connected, non-zero otherwise +if ! tailscale status >/dev/null 2>&1; then + echo "Tailscale is down or unauthenticated. Running 'up'..." + + read -p "Enter the auth key. Please contact a software lead if you don't have one: " AUTH_KEY + + sudo tailscale up --auth-key=$AUTH_KEY +fi diff --git a/scripts/mezzsh/utils/connection_warn.sh b/scripts/mezzsh/utils/connection_warn.sh new file mode 100644 index 0000000000..219c651dea --- /dev/null +++ b/scripts/mezzsh/utils/connection_warn.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Script to trigger a dialog on the Mezz PC monitor that someone +# has just connected remotely + +# Finds IRL users +LOCAL_USER=$(bash /home/thunderbots/Software/scripts/mezzsh/utils/get_local_user.sh) + +# if no one is logged in locally, just exit +if [ -z "$LOCAL_USER" ]; then + exit 0 +fi + +# get the User ID of the local user +LOCAL_UID=$(id -u "$LOCAL_USER") + +# Trigger the dialog +# these environment variables must be set to trigger the dialog +# from a background script (i.e our SSH server script) +# specifically on our wayland setup +sudo -u "$LOCAL_USER" DISPLAY=tty2 \ + DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$LOCAL_UID/bus \ + XDG_RUNTIME_DIR="/run/user/$LOCAL_UID" \ + WAYLAND_DISPLAY="wayland-0" \ + GDK_BACKEND="wayland" \ + zenity --warning --title="Remote Connection" \ + --text="A new has just connected remotely (via SSH)." --timeout=10 & diff --git a/scripts/mezzsh/utils/get_connected_users.sh b/scripts/mezzsh/utils/get_connected_users.sh new file mode 100644 index 0000000000..b14c17e8f0 --- /dev/null +++ b/scripts/mezzsh/utils/get_connected_users.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Gets the names of all currently connected SSH users +# Each user has their name attached to their SSH public key in authorized_keys +# So we find the key each current connection used to connect, and then the corresponding name + +# Get keys and comments from authorized_keys +KEYS_FILE="/home/thunderbots/.ssh/authorized_keys" +declare -A key_to_names + +while read -r line; do + # Skip the line if it doesn't start with "ssh" + # to skip over the extra flags attached to each key on separate lines + [[ ! $line =~ ^ssh ]] && continue + + # the line consists of + + # Get fingerprint for the ssh key + fingerprint=$(echo "$line" | ssh-keygen -l -f - | awk '{print $2}') + + # get the username part of the line + username=$(echo "$line" | awk '{print $NF}') + + # map fingerprint to username + key_to_names["$fingerprint"]="$username" +done < "$KEYS_FILE" + +# Find currently active SSH PIDs and match them to log entries +pgrep -u "root" sshd | tail -n +2 | while read -r pid; do + # Look for the 'Accepted publickey' log entry for this specific PID + # there should be a log entry with the text "sshd[] + # log entry contains the fingerprint within the text "SHA256: + fingerprint_match=$(grep "sshd\[$pid\]" /var/log/auth.log | grep "Accepted publickey" | grep -oE "SHA256:[^ ]+") + + if [ -n "$fingerprint_match" ]; then + username=${key_to_names[$fingerprint_match]} + echo "${username}" + fi +done diff --git a/scripts/mezzsh/utils/get_local_user.sh b/scripts/mezzsh/utils/get_local_user.sh new file mode 100644 index 0000000000..03f3842968 --- /dev/null +++ b/scripts/mezzsh/utils/get_local_user.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Script to check if there's a person logged in physically +# at the Mezz PC +# The `who` command returns all logged in users (IRL and remote) +# Finds IRL users by looking for the physical monitor (tty2) +who | grep -m 1 "(tty2)" | awk '{print $1}'