From bdc2cdbadb5b6645f645d17b09d33b962d477eea Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 17 Aug 2026 05:44:35 -0400 Subject: [PATCH] Clear the onboard phase flags on offboard, and let the mirror signal the owner before revoking the key it signals with --- Partnership/partnership_offboard.sh | 43 ++++++++++++++++++++++++++--- common.sh | 16 +++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/Partnership/partnership_offboard.sh b/Partnership/partnership_offboard.sh index cfeed73..5580d30 100755 --- a/Partnership/partnership_offboard.sh +++ b/Partnership/partnership_offboard.sh @@ -37,7 +37,8 @@ # Step 5: Disarm sync gates — same four gates as the owner path # Step 6: Revoke Emby admin — remove own admin account from local Emby instance # Step 7: Restart own stack — bring up own parked containers -# Step 8: SSH revocation — revoke keys both directions, write state, signal owner +# Step 8: Finalise — write INACTIVE, clear phase flags, signal owner, THEN revoke +# keys. Revocation is last because the signal needs the key # # ============================================================================================== # DESIGN PRINCIPLES @@ -406,19 +407,31 @@ if [[ "$AM_MIRROR" == true ]]; then echo "" echo "━━━ $ICON_SHIELD Step 8/8 — SSH Revocation + State ━━━" - do_ssh_key_revocation "${OWNER_IP:-}" - + # State first, keys last — same ordering the owner path needed. Revocation used to run here, + # before the push below, so the mirror destroyed the key and then tried to tell the owner it + # had left using that key. The owner never heard, and the notify promised it would "finalise + # on next check" — a check that now had no way in. NOW=$(date '+%Y-%m-%d %H:%M:%S') if [[ "$DRY_RUN" == false ]]; then write_state_file "$LOCAL_STATE_FILE" \ "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON" echo "Local state: INACTIVE ✅" add_to_blocklist "$OWNER" "$REASON" + + # Inverse of onboard's write_onboard_phase. MIRROR_ID is this host on this path, and the + # flags are named for the mirror on both sides, so the same names clear here. + _setup_db="$(platform_setup_db_path)" + for _flag in "${MIRROR_ID}_PHASE1_DONE" "${MIRROR_ID}_PHASE2_DONE" "${MIRROR_ID}_KEY_READY"; do + clear_state_var "$_setup_db" "$_flag" + done + echo "Onboard phase flags cleared ✅" + unset _setup_db _flag else - warn "DRY RUN — would write INACTIVE state and blocklist $OWNER" + warn "DRY RUN — would write INACTIVE state, clear phase flags and blocklist $OWNER" fi if [[ "$OWNER_REACHABLE" == true ]]; then + platform_push_setup_state 2>/dev/null || warn "Could not push cleared setup state to $OWNER" push_state_to_remote "$LOCAL_STATE_FILE" "$OWNER_IP" "$OWNER_SSH_KEY" notify "Partnership offboard requested by $MIRROR — $OWNER will finalise on next check" \ "Partnership" "normal" @@ -426,6 +439,9 @@ if [[ "$AM_MIRROR" == true ]]; then warn "$OWNER unreachable — state written locally, owner will see it when reachable" fi + # Last, for the reason above: everything before it needs the key. + do_ssh_key_revocation "${OWNER_IP:-}" + # ── Summary ─────────────────────────────────────────────────────────────────────────────── END=$(date +%s) echo "" @@ -605,7 +621,26 @@ if [[ "$DRY_RUN" == false ]]; then STEP_STATE_WRITE_OK=false fi add_to_blocklist "$MIRROR" "$REASON" + + # The inverse of onboard's write_onboard_phase, which had none. Without this a completed + # offboard leaves the state file reading INACTIVE beside HOST*_PHASE1_DONE / _PHASE2_DONE + # still set — and the setup checklist and partnership card read the flags, not the state + # file, so a torn-down partnership went on presenting itself as fully onboarded. + # + # KEY_READY goes too: it means "a key is generated and waiting to be installed", which stops + # being true the moment Step 11 revokes both sides. + _setup_db="$(platform_setup_db_path)" + for _flag in "${MIRROR_ID}_PHASE1_DONE" "${MIRROR_ID}_PHASE2_DONE" "${MIRROR_ID}_KEY_READY"; do + clear_state_var "$_setup_db" "$_flag" + done + echo "Onboard phase flags cleared ✅" + unset _setup_db _flag + if [[ "$MIRROR_REACHABLE" == true ]]; then + # Pushed after the flags are cleared, so the mirror receives the cleared file rather than + # the version that still claimed a finished onboard. + platform_push_setup_state 2>/dev/null || \ + warn "Could not push cleared setup state to $MIRROR" if push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then STEP_STATE_PUSH_OK=true else diff --git a/common.sh b/common.sh index 214db05..2485ff8 100755 --- a/common.sh +++ b/common.sh @@ -996,6 +996,22 @@ set_state_var() { fi } +# Removes a key from a flat state file. The inverse of set_state_var, and the reason it exists: +# onboarding writes HOST*_PHASE*_DONE and nothing took them away again, so a completed offboard +# left a state file reading INACTIVE beside flags still claiming a finished onboard. Anything +# reading the flags — the setup checklist, the partnership card — believed the torn-down +# partnership was live. +# +# Absent key is success, not an error: clearing twice is how a re-run after a partial teardown +# should behave. +# Usage: clear_state_var "$state_file" "$key" +clear_state_var() { + local file="$1" key="$2" + [[ -f "$file" ]] || return 0 + grep -q "^${key}=" "$file" 2>/dev/null || return 0 + sed -i "/^${key}=/d" "$file" +} + # ============================================================================================== # ── CONNECTIVITY CHECKS ─────────────────────────────────────────────────────────────────────── # ==============================================================================================