Add SSH key revocation to offboard flow

On offboard, neither server should retain SSH access to the other.
Keys are now revoked as the final step before Tailscale removal — after
all state pushes and syncs are complete so no SSH operation is cut short.

Revocation runs in both owner-initiated offboard and when --check
finalises a mirror-requested offboard. Both directions are handled:
  - Our pubkey removed from remote's authorized_keys via SSH (identified
    by key comment keyname@hostname set by ssh_setup.sh at keygen time)
  - Remote's pubkey removed from local authorized_keys (matched by
    @REMOTE_SERVER_NAME suffix — unique across the authorized_keys file)

Non-fatal: SSH failure logs a manual revocation instruction; local
cleanup still runs. Summary shows per-direction revocation status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gmer4Lfe
2026-05-09 10:21:01 -04:00
co-authored by Claude Sonnet 4.6
parent 4821dbdfb4
commit 6674dd4c17
+88
View File
@@ -384,6 +384,72 @@ remove_from_blocklist() {
log "Removed from blocklist: $hostname"
}
# Revoke SSH access on both sides — call after all other SSH operations complete.
# Matches by key comment (format: keyname@hostname — set by ssh_setup.sh at keygen time).
# SSH_REVOKE_REMOTE_OK / SSH_REVOKE_LOCAL_OK set in caller scope for summary display.
do_ssh_key_revocation() {
local remote_ip="$1"
local pub_key_file="${SSH_KEY}.pub"
echo ""
echo "━━━ $ICON_SHIELD SSH Key Revocation ━━━"
# Step 1: remove our pubkey from remote's authorized_keys while SSH still works
SSH_REVOKE_REMOTE_OK=false
if [[ -f "$pub_key_file" ]]; then
local our_comment
our_comment=$(awk '{print $3}' "$pub_key_file" 2>/dev/null)
if [[ -n "$our_comment" ]]; then
log "Revoking our pubkey ($our_comment) from $REMOTE_SERVER_NAME..."
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove our pubkey from $REMOTE_SERVER_NAME authorized_keys"
SSH_REVOKE_REMOTE_OK=true
elif [[ -n "$remote_ip" ]] && timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
"grep -v '${our_comment}' /root/.ssh/authorized_keys \
> /root/.ssh/authorized_keys.tmp 2>/dev/null \
&& mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys \
&& echo removed" 2>/dev/null | grep -q removed; then
log "Our pubkey revoked from $REMOTE_SERVER_NAME"
SSH_REVOKE_REMOTE_OK=true
else
warn "Remote revocation failed — revoke manually on $REMOTE_SERVER_NAME:"
warn " grep -v '@${LOCAL_SERVER_NAME}' /root/.ssh/authorized_keys > /root/.ssh/authorized_keys"
fi
else
warn "Could not read pubkey comment from $pub_key_file — skipping remote revocation"
fi
else
warn "Pubkey not found at $pub_key_file — skipping remote revocation"
fi
# Step 2: remove remote's pubkey from our local authorized_keys
# Remote key comment ends with @REMOTE_SERVER_NAME — unique, no special chars
SSH_REVOKE_LOCAL_OK=false
log "Revoking $REMOTE_SERVER_NAME pubkey from local authorized_keys..."
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove $REMOTE_SERVER_NAME pubkey from local authorized_keys"
SSH_REVOKE_LOCAL_OK=true
elif [[ -f /root/.ssh/authorized_keys ]]; then
if grep -q "@${REMOTE_SERVER_NAME}" /root/.ssh/authorized_keys 2>/dev/null; then
if grep -v "@${REMOTE_SERVER_NAME}" /root/.ssh/authorized_keys \
> /root/.ssh/authorized_keys.tmp 2>/dev/null && \
mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys; then
log "$REMOTE_SERVER_NAME pubkey revoked locally ✅"
SSH_REVOKE_LOCAL_OK=true
else
warn "Failed to update local authorized_keys — remove @${REMOTE_SERVER_NAME} entry manually"
fi
else
log "$REMOTE_SERVER_NAME pubkey not found in local authorized_keys — already removed"
SSH_REVOKE_LOCAL_OK=true
fi
else
log "/root/.ssh/authorized_keys not found — nothing to remove locally"
SSH_REVOKE_LOCAL_OK=true
fi
}
# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"
@@ -826,6 +892,12 @@ if [[ "$MODE" == "check" ]]; then
update_master_conf "CRITICAL_RSYNC_ENABLED" "false"
fi
# SSH key revocation — mutual, both directions
# REMOTE_IP already resolved above — this is the last SSH operation
SSH_REVOKE_REMOTE_OK=false
SSH_REVOKE_LOCAL_OK=false
do_ssh_key_revocation "$REMOTE_IP"
if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
local grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-6} * 3600 ))
warn "Waiting ${PARTNERSHIP_GRACE_HOURS:-6}hr grace period..."
@@ -1135,6 +1207,12 @@ if [[ "$MODE" == "offboard" ]]; then
folderview3_remove_partner_folder "$PARTNER_FOLDER_NAME"
fi
# SSH key revocation — mutual, both directions
# Must run before Tailscale removal (SSH needs network) and after state is pushed
SSH_REVOKE_REMOTE_OK=false
SSH_REVOKE_LOCAL_OK=false
do_ssh_key_revocation "${MIRROR_IP:-}"
# Tailscale removal
if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
echo ""
@@ -1174,6 +1252,16 @@ if [[ "$MODE" == "offboard" ]]; then
echo " Critical rsync: disabled ✅"
echo " State: INACTIVE ✅"
echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
_revoke_status() {
if [[ "${SSH_REVOKE_REMOTE_OK:-false}" == true ]] && [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
echo "both directions ✅"
elif [[ "${SSH_REVOKE_LOCAL_OK:-false}" == true ]]; then
echo "local only ✅ — remote failed (revoke manually on $MIRROR)"
else
echo "⚠️ failed — check warnings above"
fi
}
echo " Keys revoked: $(_revoke_status)"
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
echo " FolderView3: ${PARTNER_FOLDER_NAME:-} cleaned ✅"
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \