15 Commits

Author SHA1 Message Date
finn 5941f95b00 slight addition to pre key verification section 2026-04-19 14:18:52 -07:00
finn 747c8a81d8 Merge remote-tracking branch 'origin/master' into onboard-spoke-logic 2026-04-19 14:18:21 -07:00
Justin Oros 5a9e55b673 setup-spoke.sh: replace manual hub instructions with onboard-spoke.sh next step prompt 2026-04-19 14:09:30 -07:00
Justin Oros e5bdf95dcf setup-spoke.sh: replace ~ with full paths and clarify hub user in completion message 2026-04-19 14:07:17 -07:00
finn 1f4e8555da Merge branch 'master' into onboard-spoke-logic 2026-04-19 14:05:54 -07:00
finn 56325a1b06 changed onboard-spoke flow 2026-04-19 14:04:12 -07:00
Justin Oros 0553420d04 setup-spoke.sh: add docker-cli to apt install list 2026-04-19 14:00:25 -07:00
Justin Oros 4cdddd649d setup-spoke.sh: add option to choose existing key from ~/.ssh/ in SSH key setup menu 2026-04-19 13:52:29 -07:00
Justin Oros 0fd7d94d58 setup-spoke.sh: redirect find_free_port warn output to stderr to prevent contamination of TUNNEL_PORT variable 2026-04-19 13:46:07 -07:00
Justin Oros f3c9cf2344 setup-spoke.sh: change password auth disable default to N and add warning to wait until after onboard-spoke.sh 2026-04-19 13:14:52 -07:00
Justin Oros f486795154 onboard-spoke.sh: add key selection prompt for tunnel auth, use explicit -i flag for all SSH calls, clarify hub key installation header 2026-04-19 13:05:29 -07:00
Justin Oros fe3f2c5b77 Update readme 2026-04-19 12:52:40 -07:00
Justin Oros 4e1e9282ac setup-spoke.sh: use docker.io and docker-compose instead of docker-compose-plugin for apt installs 2026-04-19 12:37:19 -07:00
Justin Oros 07f4601bad setup-spoke.sh: replace docker.io apt install with Docker official install script to fix docker-compose-plugin availability 2026-04-19 11:45:07 -07:00
Justin Oros 9bdd12ebbd onboard-spoke.sh: add rclone auto-mount via crontab @reboot entry and immediate mount on onboarding 2026-04-19 11:40:47 -07:00
3 changed files with 193 additions and 125 deletions
+18
View File
@@ -35,6 +35,24 @@ cd tinyboard
./setup.sh # option 1 (configure new spoke) ./setup.sh # option 1 (configure new spoke)
``` ```
### Adding the Spoke's Public Key to the Hub
During `setup-spoke.sh`, a key pair is generated on the spoke for the autossh tunnel. The script will display the public key and pause. Before pressing ENTER, the hub owner must add the public key to the hub user's `authorized_keys`. Run this on the hub as the hub user (e.g. `armbian`):
```bash
echo "<paste public key here>" >> ~/.ssh/authorized_keys
```
Or as root:
```bash
echo "<paste public key here>" >> /home/armbian/.ssh/authorized_keys
```
Once the key is added, press ENTER on the spoke to continue. The script will test the SSH connection and if successful, bring up the tunnel.
The private key never leaves the spoke — only the public key is shared.
### Onboarding a Spoke from the Hub ### Onboarding a Spoke from the Hub
Once the spoke tunnel is up, run on the hub: Once the spoke tunnel is up, run on the hub:
+53 -21
View File
@@ -13,8 +13,15 @@ NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; } info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } die() {
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; } echo -e "${RED}[ERROR]${NC} $*" >&2
exit 1
}
header() {
echo -e "\n${CYAN}══════════════════════════════════════════${NC}"
echo -e "${CYAN} $*${NC}"
echo -e "${CYAN}══════════════════════════════════════════${NC}"
}
check_deps() { check_deps() {
local missing=() local missing=()
@@ -74,33 +81,33 @@ KEY_PATH="$SSH_DIR/$KEY_NAME"
mkdir -p "$(dirname "$RCLONE_CONF")" mkdir -p "$(dirname "$RCLONE_CONF")"
header "Checking Tunnel" header "Checking Tunnel"
info "Verifying spoke SSH service is reachable on port $TUNNEL_PORT..."
# Test TCP connectivity first
if ! timeout 5 bash -c "cat < /dev/null > /dev/tcp/localhost/$TUNNEL_PORT" 2>/dev/null; then
die "Cannot connect to port $TUNNEL_PORT on localhost — is the tunnel up?"
fi
info "Scanning spoke host key..." info "Scanning spoke host key..."
KEYSCAN=$(ssh-keyscan -p "$TUNNEL_PORT" -H localhost 2>/dev/null) KEYSCAN=$(ssh-keyscan -p "$TUNNEL_PORT" -H localhost 2>/dev/null)
[ -n "$KEYSCAN" ] || die "Spoke not reachable on port $TUNNEL_PORT — is the tunnel up?" [ -n "$KEYSCAN" ] || die "Spoke not reachable on port $TUNNEL_PORT — is the tunnel up?"
while IFS= read -r KEYSCAN_LINE; do while IFS= read -r KEYSCAN_LINE; do
KEYSCAN_KEY=$(echo "$KEYSCAN_LINE" | awk '{print $2, $3}') KEYSCAN_KEY=$(echo "$KEYSCAN_LINE" | awk '{print $2, $3}')
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
echo "$KEYSCAN_LINE" >> "$SSH_DIR/known_hosts" echo "$KEYSCAN_LINE" >>"$SSH_DIR/known_hosts"
fi fi
done <<< "$KEYSCAN" done <<<"$KEYSCAN"
info "Verifying spoke is reachable on port $TUNNEL_PORT..."
retry_or_abort \
"ssh -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \
"Spoke not reachable on port $TUNNEL_PORT. Make sure the tunnel is up."
header "Generating Hub SSH Key" header "Generating Hub SSH Key"
if [ -f "$KEY_PATH" ]; then if [ -f "$KEY_PATH" ]; then
warn "Key $KEY_PATH already exists, skipping generation." warn "Key $KEY_PATH already exists, skipping generation."
else else
ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "$KEY_NAME"
info "Key generated: $KEY_PATH" info "Key generated: $KEY_PATH"
fi fi
chmod 600 "$KEY_PATH" chmod 600 "$KEY_PATH"
info "Permissions set: $KEY_PATH is 600" info "Permissions set: $KEY_PATH is 600"
header "Copying Hub Key to Spoke" header "Installing Hub Access Key on Spoke"
info "Running ssh-copy-id to $SPOKE_USER@localhost:$TUNNEL_PORT..." info "Copying hub public key to spoke's authorized_keys so the hub can SSH in for rclone..."
info "(You will be prompted for the $SPOKE_USER password on the spoke)" info "(You will be prompted for the $SPOKE_USER password on the spoke)"
if ssh-copy-id -i "$KEY_PATH.pub" -p "$TUNNEL_PORT" "$SPOKE_USER"@localhost; then if ssh-copy-id -i "$KEY_PATH.pub" -p "$TUNNEL_PORT" "$SPOKE_USER"@localhost; then
info "Key copied." info "Key copied."
@@ -125,8 +132,8 @@ header "Adding rclone Remote"
if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping." warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping."
else else
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF" [ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >>"$RCLONE_CONF"
cat >> "$RCLONE_CONF" <<EOF cat >>"$RCLONE_CONF" <<EOF
[${SPOKE_NAME}-remote] [${SPOKE_NAME}-remote]
type = sftp type = sftp
@@ -161,7 +168,10 @@ if [[ "${ADD_UNION,,}" == "y" ]]; then
1) UPSTREAM_TAG=":ro" ;; 1) UPSTREAM_TAG=":ro" ;;
2) UPSTREAM_TAG=":nc" ;; 2) UPSTREAM_TAG=":nc" ;;
3) UPSTREAM_TAG=":writeback" ;; 3) UPSTREAM_TAG=":writeback" ;;
*) warn "Invalid choice, defaulting to full read/write."; UPSTREAM_TAG="" ;; *)
warn "Invalid choice, defaulting to full read/write."
UPSTREAM_TAG=""
;;
esac esac
if [ -n "$UNION_PATH" ]; then if [ -n "$UNION_PATH" ]; then
UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}" UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}"
@@ -169,7 +179,8 @@ if [[ "${ADD_UNION,,}" == "y" ]]; then
UPSTREAM="${SPOKE_NAME}-remote:${UPSTREAM_TAG}" UPSTREAM="${SPOKE_NAME}-remote:${UPSTREAM_TAG}"
fi fi
if grep -q "^\[${UNION_NAME}\]" "$RCLONE_CONF" 2>/dev/null; then if grep -q "^\[${UNION_NAME}\]" "$RCLONE_CONF" 2>/dev/null; then
ALREADY=$(python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF' ALREADY=$(
python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF'
import sys import sys
path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3] path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path) as f: with open(path) as f:
@@ -185,7 +196,7 @@ for line in lines:
sys.exit(0) sys.exit(0)
print("no") print("no")
PYEOF PYEOF
) )
if [ "$ALREADY" = "yes" ]; then if [ "$ALREADY" = "yes" ]; then
warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping." warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping."
else else
@@ -210,8 +221,8 @@ PYEOF
info "Added '$UPSTREAM' to union remote [${UNION_NAME}]." info "Added '$UPSTREAM' to union remote [${UNION_NAME}]."
fi fi
else else
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF" [ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >>"$RCLONE_CONF"
printf '\n[%s]\ntype = union\nupstreams = %s\n' "$UNION_NAME" "$UPSTREAM" >> "$RCLONE_CONF" printf '\n[%s]\ntype = union\nupstreams = %s\n' "$UNION_NAME" "$UPSTREAM" >>"$RCLONE_CONF"
info "Union remote [${UNION_NAME}] created with upstream '$UPSTREAM'." info "Union remote [${UNION_NAME}] created with upstream '$UPSTREAM'."
fi fi
fi fi
@@ -229,12 +240,33 @@ MOUNT_POINT="${HOME}/mnt/${SPOKE_NAME}"
mkdir -p "$MOUNT_POINT" mkdir -p "$MOUNT_POINT"
if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then
warn "$SPOKE_NAME already in registry, updating." warn "$SPOKE_NAME already in registry, updating."
grep -v "^${SPOKE_NAME} " "$REGISTRY" > "${REGISTRY}.tmp" 2>/dev/null || true grep -v "^${SPOKE_NAME} " "$REGISTRY" >"${REGISTRY}.tmp" 2>/dev/null || true
mv "${REGISTRY}.tmp" "$REGISTRY" mv "${REGISTRY}.tmp" "$REGISTRY"
fi fi
echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >> "$REGISTRY" echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >>"$REGISTRY"
info "$SPOKE_NAME registered." info "$SPOKE_NAME registered."
header "Setting Up Auto-Mount"
MOUNT_CMD="rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode writes --allow-other --daemon"
CRON_ENTRY="@reboot ${MOUNT_CMD}"
EXISTING=$(crontab -l 2>/dev/null || true)
if echo "$EXISTING" | grep -qF "${SPOKE_NAME}-remote:"; then
warn "Crontab entry for ${SPOKE_NAME}-remote already exists, skipping."
else
CRONTAB_BACKUP="${HOME}/.config/tinyboard/crontab.$(date +%Y%m%d%H%M%S)"
mkdir -p "$(dirname "$CRONTAB_BACKUP")"
echo "$EXISTING" >"$CRONTAB_BACKUP"
info "Crontab backed up to $CRONTAB_BACKUP"
{
echo "$EXISTING"
echo "$CRON_ENTRY"
} | crontab -
info "Auto-mount crontab entry added for ${SPOKE_NAME}."
fi
info "Starting mount now..."
mkdir -p "$MOUNT_POINT"
eval "$MOUNT_CMD" 2>/dev/null && info "Mounted ${SPOKE_NAME} at ${MOUNT_POINT}." || warn "Mount failed — will retry on next reboot."
header "Onboarding Complete" header "Onboarding Complete"
echo -e " Spoke: ${GREEN}$SPOKE_NAME${NC}" echo -e " Spoke: ${GREEN}$SPOKE_NAME${NC}"
echo -e " Port: ${GREEN}$TUNNEL_PORT${NC}" echo -e " Port: ${GREEN}$TUNNEL_PORT${NC}"
+41 -23
View File
@@ -136,7 +136,7 @@ $PKG_INSTALL vim "$AUTOSSH_PKG" "$OPENSSH_PKG" git
info "Installing Docker..." info "Installing Docker..."
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
if [ "$PKG_MANAGER" = "apt" ]; then if [ "$PKG_MANAGER" = "apt" ]; then
$PKG_INSTALL docker.io docker-compose-plugin $PKG_INSTALL docker.io docker-cli docker-compose
else else
curl -fsSL https://get.docker.com | bash curl -fsSL https://get.docker.com | bash
fi fi
@@ -146,7 +146,7 @@ fi
if ! docker compose version >/dev/null 2>&1; then if ! docker compose version >/dev/null 2>&1; then
if [ "$PKG_MANAGER" = "apt" ]; then if [ "$PKG_MANAGER" = "apt" ]; then
$PKG_INSTALL docker-compose-plugin $PKG_INSTALL docker-compose
else else
warn "docker compose not available — Docker install script should have included it." warn "docker compose not available — Docker install script should have included it."
fi fi
@@ -184,9 +184,10 @@ info "Hostname set to: $SPOKE_NAME"
header "SSH Key Setup" header "SSH Key Setup"
echo "How would you like to handle the SSH key for the tunnel to $HUB_HOST?" echo "How would you like to handle the SSH key for the tunnel to $HUB_HOST?"
echo " 1) Generate a new key automatically" echo " 1) Generate a new key automatically"
echo " 2) Use an existing key (paste the private key)" echo " 2) Choose an existing key from $SSH_DIR"
echo " 3) Paste a private key manually"
echo "" echo ""
read -rp "Choose [1/2]: " KEY_CHOICE read -rp "Choose [1/2/3]: " KEY_CHOICE
case "$KEY_CHOICE" in case "$KEY_CHOICE" in
1) 1)
@@ -217,6 +218,34 @@ case "$KEY_CHOICE" in
read -rp "Press ENTER once the key has been added to ${HUB_HOST}..." read -rp "Press ENTER once the key has been added to ${HUB_HOST}..."
;; ;;
2) 2)
mkdir -p "$SSH_DIR"
chown "$SPOKE_USER":"$SPOKE_USER" "$SSH_DIR"
chmod 700 "$SSH_DIR"
AVAILABLE_KEYS=()
while IFS= read -r keyfile; do
AVAILABLE_KEYS+=("$keyfile")
done < <(find "$SSH_DIR" -maxdepth 1 -type f ! -name "*.pub" ! -name "known_hosts" ! -name "authorized_keys" ! -name "config" | sort)
if [ ${#AVAILABLE_KEYS[@]} -eq 0 ]; then
die "No private keys found in $SSH_DIR."
fi
echo "Available keys:"
for i in "${!AVAILABLE_KEYS[@]}"; do
echo " $i) ${AVAILABLE_KEYS[$i]}"
done
echo ""
read -rp "Choose key [0]: " KEY_IDX
KEY_IDX="${KEY_IDX:-0}"
[[ "$KEY_IDX" =~ ^[0-9]+$ ]] && [ "$KEY_IDX" -lt "${#AVAILABLE_KEYS[@]}" ] || die "Invalid choice."
KEY_PATH="${AVAILABLE_KEYS[$KEY_IDX]}"
KEY_NAME="$(basename "$KEY_PATH")"
info "Using existing key: $KEY_PATH"
echo ""
read -rp "Press ENTER once the public key has been added to ${HUB_HOST} authorized_keys..."
;;
3)
read -rp "Enter a name for the key file [hubkey]: " KEY_NAME read -rp "Enter a name for the key file [hubkey]: " KEY_NAME
KEY_NAME="${KEY_NAME:-hubkey}" KEY_NAME="${KEY_NAME:-hubkey}"
KEY_PATH="$SSH_DIR/$KEY_NAME" KEY_PATH="$SSH_DIR/$KEY_NAME"
@@ -237,8 +266,11 @@ case "$KEY_CHOICE" in
esac esac
header "Password Authentication" header "Password Authentication"
read -rp "Disable password auth for $SPOKE_USER and use keys only? [Y/n]: " DISABLE_PASS warn "Do not disable password auth yet — the hub still needs password access to install its key via ssh-copy-id."
DISABLE_PASS="${DISABLE_PASS:-y}" warn "Only disable this after running onboard-spoke.sh on the hub."
echo ""
read -rp "Disable password auth for $SPOKE_USER and use keys only? [y/N]: " DISABLE_PASS
DISABLE_PASS="${DISABLE_PASS:-n}"
if [[ "${DISABLE_PASS,,}" == "y" ]]; then if [[ "${DISABLE_PASS,,}" == "y" ]]; then
if [ ! -f "$KEY_PATH" ]; then if [ ! -f "$KEY_PATH" ]; then
warn "No key found at $KEY_PATH — skipping password auth disable to avoid lockout." warn "No key found at $KEY_PATH — skipping password auth disable to avoid lockout."
@@ -305,7 +337,7 @@ find_free_port() {
echo "$port" echo "$port"
return 0 return 0
fi fi
warn "Port $port is in use, trying next..." echo -e "${YELLOW}[!]${NC} Port $port is in use, trying next..." >&2
done done
return 1 return 1
} }
@@ -373,21 +405,7 @@ echo -e " Spoke name: ${GREEN}$SPOKE_NAME${NC}"
echo -e " Tunnel port: ${GREEN}$TUNNEL_PORT${NC} on $HUB_HOST" echo -e " Tunnel port: ${GREEN}$TUNNEL_PORT${NC} on $HUB_HOST"
echo -e " SSH key: ${GREEN}$KEY_PATH${NC}" echo -e " SSH key: ${GREEN}$KEY_PATH${NC}"
echo "" echo ""
echo -e "${YELLOW}The hub owner needs to do the following on ${HUB_HOST}:${NC}" echo -e "${YELLOW}Next step — on the hub, run as ${HUB_USER}:${NC}"
echo "" echo ""
echo " 1. Generate a hub->spoke key:" echo " cd tinyboard && ./setup.sh # choose option 2 (onboard spoke)"
echo " ssh-keygen -t ed25519 -f ~/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m)"
echo ""
echo " 2. Copy it to this spoke through the tunnel:"
echo " ssh-copy-id -i ~/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m).pub -p $TUNNEL_PORT ${HUB_USER}@localhost"
echo ""
echo " 3. Add an rclone remote in ~/.config/rclone/rclone.conf:"
echo " [${SPOKE_NAME}-remote]"
echo " type = sftp"
echo " host = localhost"
echo " port = $TUNNEL_PORT"
echo " key_file = /home/$HUB_USER/.ssh/${HUB_USER}-${SPOKE_NAME}-$(date +%Y%m)"
echo " shell_type = unix"
echo " md5sum_command = md5sum"
echo " sha1sum_command = sha1sum"
echo "" echo ""