#!/bin/bash # Pairing token — the out-of-band string two LibrePortal instances exchange # so each can talk to the other over SSH with a forced-command. Format: # # lp-peer|v1|||||| # # Pipe-delimited because SSH fingerprints (SHA256:base64) and base64 itself # include `:` and `=`, so colon-splitting eats fields. Pipe is safe: base64 # alphabet is [A-Za-z0-9+/=], fingerprints are SHA256:[A-Za-z0-9+/=], names # and hostnames don't contain pipes. # # - name Friendly label the OTHER side suggests for itself ("homelab"). # The accepting side may override at peer-add time. # - user Manager username on the originating host (the SSH login). # - host:port How to reach the originating host. port defaults to 22. # - pubkey base64 of "ssh-ed25519 AAAA… comment" — the originator's # peer pubkey (peerKeyPublic). # - fingerprint SHA256:… of the pubkey, for the accepting user to verify. peerPairingToken() { peerKeyEnsure || return 1 local name="${CFG_INSTALL_NAME:-$(hostname)}" local user user="$(whoami)" local host="${CFG_PUBLIC_HOSTNAME:-${CFG_INSTALL_NAME:-$(hostname)}}" local port="${CFG_SSH_PORT:-22}" local pub fp pub_b64 pub=$(peerKeyPublic) || { isError "Could not read peer pubkey"; return 1; } fp=$(peerKeyFingerprint) # base64 -w0 keeps the encoded pubkey on a single token field. The pubkey # itself already contains spaces — we MUST encode it. pub_b64=$(printf '%s' "$pub" | base64 -w0) printf 'lp-peer|v1|%s|%s|%s|%s|%s|%s\n' \ "$name" "$user" "$host" "$port" "$pub_b64" "$fp" } # Parse a token. Echoes a single-line JSON object on success, JSON {"error":…} # on failure (return 1). peerPairingParse() { local token="$1" if [[ -z "$token" || "$token" != lp-peer\|* ]]; then echo '{"error":"not-a-token"}' return 1 fi # Pipe-split. Easy now: every field is just "${arr[N]}". local IFS='|' # shellcheck disable=SC2206 local parts=( $token ) IFS=$' \t\n' if [[ ${#parts[@]} -lt 8 || "${parts[0]}" != "lp-peer" ]]; then echo '{"error":"malformed"}' return 1 fi local version="${parts[1]}" if [[ "$version" != "v1" ]]; then echo "{\"error\":\"unsupported-version\",\"version\":\"$version\"}" return 1 fi local name="${parts[2]}" local user="${parts[3]}" local host="${parts[4]}" local port="${parts[5]}" local pubkey_b64="${parts[6]}" local fp="${parts[7]}" local pubkey pubkey=$(printf '%s' "$pubkey_b64" | base64 -d 2>/dev/null) if [[ -z "$pubkey" ]] || ! printf '%s\n' "$pubkey" | ssh-keygen -l -f - >/dev/null 2>&1; then echo '{"error":"invalid-pubkey"}' return 1 fi # Re-derive fingerprint from the actual key and confirm it matches what # the token advertised — catches truncated/edited tokens. local actual_fp actual_fp=$(printf '%s' "$pubkey" | ssh-keygen -l -f - 2>/dev/null | awk '{print $2}') if [[ -n "$fp" && "$actual_fp" != "$fp" ]]; then echo "{\"error\":\"fingerprint-mismatch\",\"advertised\":\"$fp\",\"actual\":\"$actual_fp\"}" return 1 fi local pub_esc="${pubkey//\\/\\\\}"; pub_esc="${pub_esc//\"/\\\"}" printf '{"version":"v1","name":"%s","user":"%s","host":"%s","port":%s,"pubkey":"%s","fingerprint":"%s"}\n' \ "$name" "$user" "$host" "$port" "$pub_esc" "$actual_fp" } # Pull a "key":"value" string field out of a flat JSON object. No jq. _peerPairingJsonStr() { printf '%s' "$1" | grep -o "\"$2\":\"[^\"]*\"" | head -1 | cut -d'"' -f4 } # Pull a "key":N numeric field out of a flat JSON object. _peerPairingJsonNum() { printf '%s' "$1" | grep -o "\"$2\":[0-9]*" | head -1 | cut -d':' -f2 } # Accept a pairing token: validate, install peer-shell, append authorized_keys # entry with forced-command, create peers row. The local label can override # the token's suggested name (handy when there's a collision). peerPairingAccept() { local token="$1" local override_name="$2" local parsed; parsed=$(peerPairingParse "$token") if [[ "$parsed" == *'"error"'* ]]; then isError "Token rejected: $parsed" return 1 fi local name pubkey host port user name=$(_peerPairingJsonStr "$parsed" name) pubkey=$(_peerPairingJsonStr "$parsed" pubkey) host=$(_peerPairingJsonStr "$parsed" host) port=$(_peerPairingJsonNum "$parsed" port) user=$(_peerPairingJsonStr "$parsed" user) [[ -n "$override_name" ]] && name="$override_name" local nv; nv=$(peerValidateName "$name") if [[ "$nv" != "ok" ]]; then isError "Invalid peer name '$name': $nv" return 1 fi # Install peer-shell and append the authorized_keys entry (this lets the # remote peer SSH IN to us). Adding the peer to OUR DB lets us SSH OUT to # them — symmetric pairing means the user runs accept on both sides. peerInstallShell || return 1 local akf="${HOME}/.ssh/authorized_keys" mkdir -p "${HOME}/.ssh" chmod 700 "${HOME}/.ssh" touch "$akf" chmod 600 "$akf" # If the key body is already in authorized_keys (under any options), # don't double-add — but DO refresh the comment to flag it as a peer. local key_body; key_body=$(awk '{print $2}' <<< "$pubkey") if grep -qF "$key_body" "$akf"; then isNotice "Pubkey already in authorized_keys — not duplicating" else local shell_path; shell_path="${HOME}/.local/bin/peer-shell" local opts="command=\"$shell_path $name\",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-user-rc" printf '%s %s peer:%s\n' "$opts" "$pubkey" "$name" >> "$akf" isSuccessful "Authorized peer '$name' to call peer-shell" fi # Now record the peer locally so we can SSH OUT to them. peerAdd "$name" direct-ssh-direct \ "host=$host" "port=$port" "user=$user" "fingerprint=$(_peerPairingJsonStr "$parsed" fingerprint)" \ || return 1 }