diff --git a/Partnership/partnership_manager.sh b/Partnership/partnership_manager.sh index f786723..ecd6f68 100644 --- a/Partnership/partnership_manager.sh +++ b/Partnership/partnership_manager.sh @@ -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 ]] && \