mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-11-01 16:29:50 +00:00
Preserve recovery keys when local legacy cleanup cannot complete (#3359)
Separate staging-file admission from grep no-match in both shipped local cleanup paths. Resolve shared targets, require intact metadata and return incomplete results without replacing unrelated keys or caller resources. Add actual filesystem failure and caller-path controls. --- Fleet candidate `20261026T204021Z-delivery-trust` from the `delivery-trust` lane. Reviewed tip `44b0500abc90788a6fcb257a8ccf931793d2d6e3` on `main`. [How Pulse handles triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
This commit is contained in:
@@ -769,12 +769,21 @@ must not silently select local-only cleanup. An explicit `--local-only` remains
|
||||
available on each node without cluster discovery or SSH. Local keys are cleaned
|
||||
once and duplicate remote addresses once, with strict SSH host verification intact.
|
||||
|
||||
Local key replacement uses an adjacent temporary file and preserves the original
|
||||
on read/comparison/metadata/rename failure. A no-match grep result is distinct
|
||||
from a read failure; removing an all-managed file may legitimately produce an
|
||||
empty file. Only the two existing Pulse comment suffixes are eligible, and
|
||||
unrelated keys and file metadata remain intact. Conditional helper invocation
|
||||
must propagate failure rather than print a successful key-removal claim.
|
||||
Both the standalone helper and `install.sh`'s presence-gated local legacy cleanup
|
||||
resolve existing key paths to regular targets before replacement; dangling
|
||||
links, non-file targets and failed resolution are not confirmed clean. Shared
|
||||
links and peer access to their target remain intact. Local key replacement uses
|
||||
an adjacent temporary file and preserves the original on allocation, output-open,
|
||||
read, comparison, metadata or rename failure. Opening the output descriptor is
|
||||
a separate checked operation: shell redirection failure can return 1 but is not
|
||||
grep's valid no-match result. Removing an all-managed file may legitimately
|
||||
produce an empty file. Only the two existing Pulse comment suffixes are eligible;
|
||||
unrelated keys and file metadata remain intact. Already-clean targets are not
|
||||
replaced. Owned staging cleanup failure is nonzero, including after otherwise
|
||||
successful work; diagnostic wording must not promise unchanged original bytes
|
||||
in that case. Private subshell traps/descriptors must not replace the caller's.
|
||||
Conditional helper invocation must propagate failure, not successful removal.
|
||||
No cluster/API action is added to the root installer.
|
||||
|
||||
Remote cleanup resolves the fixed `authorized_keys` pathname before replacing
|
||||
its target; it must never replace a cluster-shared symlink with a private copy.
|
||||
@@ -814,6 +823,19 @@ inventory through the real cluster/remote helpers into main's incomplete result,
|
||||
without live SSH, Proxmox or customer operations. These checks do not establish
|
||||
native cluster-filesystem semantics or any original installed recovery.
|
||||
|
||||
`TestUninstallSensorProxyLocalKeyWritersPreserveRecovery` applies real private
|
||||
filesystem controls to both shipped local entrypoints, including regular/shared
|
||||
targets, peer readback, dangling/non-file paths, every writer failure and an
|
||||
actual ordinary-user mode-0400 output-open denial. Root test invocations explicitly
|
||||
skip only that permission-specific case, not the other controls.
|
||||
`TestUninstallSensorProxyLocalKeyWriterKeepsCallerResources` proves the caller's
|
||||
EXIT trap and descriptor survive. `TestUninstallSensorProxyLocalKeyFailureReachesMain`
|
||||
and `TestRootInstallSensorProxyCleanupCannotHideKeyReadFailure` execute the actual
|
||||
caller paths with closed service/file/API doubles and a partial failed key read;
|
||||
they require preserved recovery keys, nonzero status and no completion claim.
|
||||
These source checks do not establish native privileged filesystem semantics,
|
||||
installed uninstall/recovery, credential revocation or reporter acceptance.
|
||||
|
||||
### Legacy sensor cleanup requires observed stopped targets
|
||||
|
||||
The legacy sensor uninstaller must confirm each existing unit has stopped and
|
||||
|
||||
+29
-9
@@ -5754,17 +5754,37 @@ local_sensor_proxy_present() {
|
||||
remove_local_sensor_proxy_managed_keys() {
|
||||
local auth_file="$SENSOR_PROXY_AUTHORIZED_KEYS_PATH"
|
||||
local tmp_file=""
|
||||
[[ -f "$auth_file" ]] || return 0
|
||||
tmp_file=$(mktemp) || return 0
|
||||
grep -v -E '# pulse-(managed|proxy)-key$' "$auth_file" >"$tmp_file" 2>/dev/null || true
|
||||
if cmp -s "$tmp_file" "$auth_file"; then
|
||||
rm -f "$tmp_file"
|
||||
local status=0
|
||||
if [[ ! -e "$auth_file" && ! -L "$auth_file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
chmod --reference="$auth_file" "$tmp_file" 2>/dev/null || chmod 600 "$tmp_file" 2>/dev/null || true
|
||||
chown --reference="$auth_file" "$tmp_file" 2>/dev/null || true
|
||||
if ! mv "$tmp_file" "$auth_file"; then
|
||||
rm -f "$tmp_file"
|
||||
# Proxmox can link this file into the shared cluster filesystem. Resolve
|
||||
# before replacing it; broken/non-file targets are not confirmed absence.
|
||||
if ! auth_file=$(readlink -f -- "$auth_file") || [[ ! -f "$auth_file" || -L "$auth_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
# Keep traps/descriptors private and replacement on the same filesystem.
|
||||
# A failed output open must never masquerade as grep's valid no-match (1).
|
||||
if ! (
|
||||
tmp_file=$(mktemp "${auth_file}.pulse-cleanup.XXXXXX") || exit 1
|
||||
trap 'status=$?; rm -f -- "$tmp_file" || status=1; exit "$status"' EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
exec 3>"$tmp_file" || exit 1
|
||||
grep -v -E '# pulse-(managed|proxy)-key$' "$auth_file" >&3 2>/dev/null || status=$?
|
||||
exec 3>&- || exit 1
|
||||
(( status <= 1 )) || exit 1
|
||||
status=0
|
||||
cmp -s "$tmp_file" "$auth_file" || status=$?
|
||||
case "$status" in
|
||||
0) ;;
|
||||
1)
|
||||
chmod --reference="$auth_file" "$tmp_file" &&
|
||||
chown --reference="$auth_file" "$tmp_file" &&
|
||||
mv -- "$tmp_file" "$auth_file" || exit 1
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
); then
|
||||
return 1
|
||||
fi
|
||||
echo "Removed legacy pulse-sensor-proxy SSH key entries from $auth_file"
|
||||
|
||||
@@ -2587,6 +2587,41 @@ fi
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootInstallSensorProxyCleanupCannotHideKeyReadFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keys := filepath.Join(dir, "authorized_keys")
|
||||
const original = "ssh-ed25519 AAAAold # pulse-managed-key\n" + sensorProxyRecoveryKeys
|
||||
if err := os.WriteFile(keys, []byte(original), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := `source "$1"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="$FIXTURE_DIR/authorized_keys"
|
||||
local_sensor_proxy_present() { :; }
|
||||
stop_pulse_unit_for_removal() { :; }
|
||||
# Filesystem/service/user removal outside the key fixture is never executed.
|
||||
rm() {
|
||||
case "${!#}" in "$FIXTURE_DIR/authorized_keys.pulse-cleanup."*) command rm "$@";; *) :;; esac
|
||||
}
|
||||
grep() { printf 'partial key read\n'; return 2; }
|
||||
userdel() { echo unexpected-user-removal >>"$FIXTURE_DIR/actions"; }
|
||||
groupdel() { echo unexpected-group-removal >>"$FIXTURE_DIR/actions"; }
|
||||
if cleanup_local_sensor_proxy; then exit 0; else exit $?; fi
|
||||
`
|
||||
out, code := runSensorProxySafetyShell(t, repoFile("install.sh"), dir, "grep", body)
|
||||
if code == 0 || strings.Contains(out, "Removed legacy") {
|
||||
t.Fatalf("installer cleanup hid failed key read: exit %d: %s", code, out)
|
||||
}
|
||||
if got, err := os.ReadFile(keys); err != nil || string(got) != original {
|
||||
t.Fatalf("installer failure lost recovery keys: %q, %v", got, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "actions")); !os.IsNotExist(err) {
|
||||
t.Fatalf("service-user removal followed unconfirmed key cleanup: %v", err)
|
||||
}
|
||||
if leftovers, err := filepath.Glob(keys + ".pulse-cleanup.*"); err != nil || len(leftovers) != 0 {
|
||||
t.Fatalf("failed installer cleanup left staging: %v, %v", leftovers, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootInstallResetBoundsEverySystemdObservationAndMutation(t *testing.T) {
|
||||
for _, name := range []string{"reset_pulse", "read_pulse_reset_unit_state", "confirm_pulse_reset_unit_stopped"} {
|
||||
body := extractRootInstallShellFunction(t, name)
|
||||
|
||||
@@ -760,6 +760,7 @@ getent() {
|
||||
[[ "$1" == ahostsv4 ]] || return 90
|
||||
case "$2" in pve-local) echo '127.0.0.1 STREAM pve-local';; pve-remote) echo '192.0.2.11 STREAM pve-remote';; *) return 90;; esac
|
||||
}
|
||||
|
||||
disable_legacy_units() { :; }
|
||||
cleanup_stale_sensor_proxy_mounts() { :; }
|
||||
remove_legacy_files() { echo files >>"$FIXTURE_DIR/actions"; }
|
||||
@@ -778,3 +779,201 @@ if main; then exit 0; else exit $?; fi
|
||||
t.Fatalf("failed remote replacement changed recovery keys: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Both shipped entrypoints must preserve unrelated access keys. All filesystem
|
||||
// operations below are real private-user operations; only faults are injected.
|
||||
func TestUninstallSensorProxyLocalKeyWritersPreserveRecovery(t *testing.T) {
|
||||
for _, entry := range []struct{ name, script, function string }{
|
||||
{"standalone", repoFile("scripts", "uninstall-sensor-proxy.sh"), "cleanup_local_authorized_keys"},
|
||||
{"installer", repoFile("install.sh"), "remove_local_sensor_proxy_managed_keys"},
|
||||
} {
|
||||
for _, kind := range []string{"regular", "shared_link"} {
|
||||
for _, phase := range []string{"success", "unchanged", "all_managed", "missing", "directory", "readlink", "mktemp", "output_open", "grep", "cmp", "chmod", "chown", "mv", "cleanup"} {
|
||||
t.Run(entry.name+"/"+kind+"/"+phase, func(t *testing.T) {
|
||||
if phase == "output_open" && os.Geteuid() == 0 {
|
||||
t.Skip("real mode-0400 write denial requires an ordinary user")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
auth := filepath.Join(dir, "local", "authorized_keys")
|
||||
target := auth
|
||||
if kind == "shared_link" {
|
||||
target = filepath.Join(dir, "cluster keys", "authorized_keys")
|
||||
}
|
||||
for _, parent := range []string{filepath.Dir(auth), filepath.Dir(target), filepath.Join(dir, "peer")} {
|
||||
if err := os.MkdirAll(parent, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
original := "ssh-ed25519 AAAAold pulse # pulse-managed-key\n" + sensorProxyRecoveryKeys + "ssh-ed25519 AAAAold2 pulse # pulse-proxy-key\n"
|
||||
if phase == "unchanged" || phase == "cleanup" {
|
||||
original = sensorProxyRecoveryKeys
|
||||
} else if phase == "all_managed" {
|
||||
original = "ssh-ed25519 AAAAold pulse # pulse-managed-key\n"
|
||||
}
|
||||
if phase == "directory" {
|
||||
if err := os.Mkdir(target, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if phase != "missing" {
|
||||
if err := os.WriteFile(target, []byte(original), 0640); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
peer := filepath.Join(dir, "peer", "authorized_keys")
|
||||
const linkTarget = "../cluster keys/authorized_keys"
|
||||
if kind == "shared_link" {
|
||||
for _, link := range []string{auth, peer} {
|
||||
if err := os.Symlink(linkTarget, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
before, _ := os.Stat(target)
|
||||
linkBefore, _ := os.Lstat(auth)
|
||||
body := `source "$1"
|
||||
AUTHORIZED_KEYS_PATH="$FIXTURE_DIR/local/authorized_keys"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="$AUTHORIZED_KEYS_PATH"
|
||||
readlink() { [[ "$SCENARIO" != readlink ]] || return 9; command readlink "$@"; }
|
||||
mktemp() {
|
||||
[[ "$SCENARIO" != mktemp ]] || return 9
|
||||
local path
|
||||
path=$(command mktemp "$@") || return 1
|
||||
[[ "$SCENARIO" != output_open ]] || command chmod 0400 "$path" || return 1
|
||||
printf '%s\n' "$path"
|
||||
}
|
||||
grep() {
|
||||
if [[ "$SCENARIO" == grep ]]; then printf 'partial read\n'; return 2; fi
|
||||
command grep "$@"
|
||||
}
|
||||
cmp() { [[ "$SCENARIO" != cmp ]] || return 2; command cmp "$@"; }
|
||||
chmod() { case "$SCENARIO" in chmod|unchanged|cleanup) return 9;; esac; command chmod "$@"; }
|
||||
chown() { case "$SCENARIO" in chown|unchanged|cleanup) return 9;; esac; command chown "$@"; }
|
||||
mv() { case "$SCENARIO" in mv|unchanged|cleanup) return 9;; esac; command mv "$@"; }
|
||||
rm() { [[ "$SCENARIO" != cleanup ]] || return 9; command rm "$@"; }
|
||||
# Conditional invocation disables errexit in the complete helper call tree.
|
||||
if ` + entry.function + `; then exit 0; else exit $?; fi
|
||||
`
|
||||
out, code := runSensorProxySafetyShell(t, entry.script, dir, phase, body)
|
||||
success := phase == "success" || phase == "unchanged" || phase == "all_managed" || (phase == "missing" && kind == "regular")
|
||||
if (code == 0) != success || (!success && (strings.Contains(out, "[ OK ]") || strings.Contains(out, "Removed legacy"))) {
|
||||
t.Fatalf("local status mismatch: exit %d: %s", code, out)
|
||||
}
|
||||
if kind == "shared_link" {
|
||||
afterLink, err := os.Lstat(auth)
|
||||
if err != nil || !os.SameFile(linkBefore, afterLink) {
|
||||
t.Fatalf("local cleanup replaced shared-key link: %v", err)
|
||||
}
|
||||
for _, link := range []string{auth, peer} {
|
||||
if got, err := os.Readlink(link); err != nil || got != linkTarget {
|
||||
t.Fatalf("shared-key relationship changed: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if phase == "missing" {
|
||||
if _, err := os.Stat(target); !os.IsNotExist(err) {
|
||||
t.Fatalf("missing key target created: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
after, err := os.Stat(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if phase == "directory" {
|
||||
if !after.IsDir() || !os.SameFile(before, after) {
|
||||
t.Fatal("non-file target was replaced")
|
||||
}
|
||||
return
|
||||
}
|
||||
want := original
|
||||
if phase == "success" {
|
||||
want = sensorProxyRecoveryKeys
|
||||
} else if phase == "all_managed" {
|
||||
want = ""
|
||||
}
|
||||
for _, path := range []string{auth, target} {
|
||||
if got, err := os.ReadFile(path); err != nil || string(got) != want {
|
||||
t.Fatalf("local recovery keys corrupted: %q, %v; want %q", got, err, want)
|
||||
}
|
||||
}
|
||||
if kind == "shared_link" {
|
||||
if got, err := os.ReadFile(peer); err != nil || string(got) != want {
|
||||
t.Fatalf("peer sees different shared keys: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
if after.Mode().Perm() != before.Mode().Perm() {
|
||||
t.Fatal("local key permissions changed")
|
||||
}
|
||||
if phase != "success" && phase != "all_managed" && !os.SameFile(before, after) {
|
||||
t.Fatal("failed or unchanged key target was replaced")
|
||||
}
|
||||
leftovers, err := filepath.Glob(target + ".pulse-cleanup.*")
|
||||
if err != nil || (phase != "cleanup" && len(leftovers) != 0) || (phase == "cleanup" && len(leftovers) != 1) {
|
||||
t.Fatalf("unexpected staging cleanup result: %v, %v", leftovers, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallSensorProxyLocalKeyWriterKeepsCallerResources(t *testing.T) {
|
||||
for _, entry := range []struct{ name, script, function string }{
|
||||
{"standalone", repoFile("scripts", "uninstall-sensor-proxy.sh"), "cleanup_local_authorized_keys"},
|
||||
{"installer", repoFile("install.sh"), "remove_local_sensor_proxy_managed_keys"},
|
||||
} {
|
||||
t.Run(entry.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
body := `source "$1"
|
||||
AUTHORIZED_KEYS_PATH="$FIXTURE_DIR/authorized_keys"
|
||||
SENSOR_PROXY_AUTHORIZED_KEYS_PATH="$AUTHORIZED_KEYS_PATH"
|
||||
printf 'ssh-ed25519 AAAAold # pulse-managed-key\n' >"$AUTHORIZED_KEYS_PATH"
|
||||
trap 'printf caller-exit >"$FIXTURE_DIR/trap"' EXIT
|
||||
exec 3>"$FIXTURE_DIR/descriptor"
|
||||
if ` + entry.function + `; then
|
||||
printf caller-still-open >&3
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
`
|
||||
out, code := runSensorProxySafetyShell(t, entry.script, dir, "success", body)
|
||||
if code != 0 {
|
||||
t.Fatalf("caller did not survive cleanup: exit %d: %s", code, out)
|
||||
}
|
||||
for file, want := range map[string]string{"trap": "caller-exit", "descriptor": "caller-still-open"} {
|
||||
if got, err := os.ReadFile(filepath.Join(dir, file)); err != nil || string(got) != want {
|
||||
t.Fatalf("caller's %s changed: %q, %v", file, got, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUninstallSensorProxyLocalKeyFailureReachesMain(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
keys := filepath.Join(dir, "authorized_keys")
|
||||
const original = "ssh-ed25519 AAAAold # pulse-managed-key\n" + sensorProxyRecoveryKeys
|
||||
if err := os.WriteFile(keys, []byte(original), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := `source "$1"
|
||||
AUTHORIZED_KEYS_PATH="$FIXTURE_DIR/authorized_keys"
|
||||
disable_legacy_units() { :; }
|
||||
cleanup_stale_sensor_proxy_mounts() { :; }
|
||||
remove_legacy_files() { echo files >>"$FIXTURE_DIR/actions"; }
|
||||
remove_proxmox_access() { :; }
|
||||
systemctl_if_available() { :; }
|
||||
grep() { printf 'partial key read\n'; return 2; }
|
||||
if main --local-only; then exit 0; else exit $?; fi
|
||||
`
|
||||
out, code := runSensorProxySafetyShell(t, repoFile("scripts", "uninstall-sensor-proxy.sh"), dir, "grep", body)
|
||||
if code == 0 || strings.Contains(out, "[ OK ]") || !strings.Contains(out, "cluster SSH key cleanup is incomplete") {
|
||||
t.Fatalf("main hid local key failure: exit %d: %s", code, out)
|
||||
}
|
||||
if got, err := os.ReadFile(keys); err != nil || string(got) != original {
|
||||
t.Fatalf("main lost recovery keys after read failure: %q, %v", got, err)
|
||||
}
|
||||
if got, err := os.ReadFile(filepath.Join(dir, "actions")); err != nil || string(got) != "files\n" {
|
||||
t.Fatalf("local failure did not reach the final main result: %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,43 +135,52 @@ disable_legacy_units() {
|
||||
return "$result"
|
||||
}
|
||||
|
||||
remove_managed_keys_from_authorized_keys_file() {
|
||||
remove_managed_keys_from_authorized_keys_file() (
|
||||
local file="$1"
|
||||
local tmp_file=""
|
||||
local status=0
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
if [[ ! -e "$file" && ! -L "$file" ]]; then
|
||||
return 0
|
||||
fi
|
||||
[[ -f "$file" && ! -L "$file" ]] || return 1
|
||||
|
||||
# Keep replacement on the same filesystem. A failed read, metadata copy or
|
||||
# rename must leave the original keys intact, even under conditional calls.
|
||||
# Isolate the descriptor and traps from the caller. Open separately: a
|
||||
# failed shell redirection returns 1 too, but is not grep's valid no-match.
|
||||
tmp_file=$(mktemp "${file}.pulse-cleanup.XXXXXX") || return 1
|
||||
grep -v -E '# pulse-(managed|proxy)-key$' "$file" >"$tmp_file" 2>/dev/null || status=$?
|
||||
if (( status > 1 )); then
|
||||
rm -f -- "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
trap 'status=$?; rm -f -- "$tmp_file" || status=1; exit "$status"' EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
exec 3>"$tmp_file" || return 1
|
||||
grep -v -E '# pulse-(managed|proxy)-key$' "$file" >&3 2>/dev/null || status=$?
|
||||
exec 3>&- || return 1
|
||||
(( status <= 1 )) || return 1
|
||||
status=0
|
||||
cmp -s "$tmp_file" "$file" || status=$?
|
||||
if (( status == 0 )); then
|
||||
rm -f -- "$tmp_file" || return 1
|
||||
return 0
|
||||
fi
|
||||
if (( status > 1 )) ||
|
||||
! chmod --reference="$file" "$tmp_file" ||
|
||||
! chown --reference="$file" "$tmp_file" ||
|
||||
! mv -- "$tmp_file" "$file"; then
|
||||
rm -f -- "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
)
|
||||
|
||||
cleanup_local_authorized_keys() {
|
||||
local auth_file=""
|
||||
auth_file=$(resolve_path "$AUTHORIZED_KEYS_PATH") || return 1
|
||||
local auth_file="$AUTHORIZED_KEYS_PATH"
|
||||
# An absent file is an idempotent no-op, but a dangling/shared link or a
|
||||
# non-file target is not confirmed clean. Never replace an unresolved link.
|
||||
if [[ -e "$auth_file" || -L "$auth_file" ]]; then
|
||||
if ! auth_file=$(readlink -f -- "$auth_file") || [[ ! -f "$auth_file" || -L "$auth_file" ]]; then
|
||||
print_warn "Local legacy SSH key path is unresolved or not a regular file; cleanup is incomplete"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if ! remove_managed_keys_from_authorized_keys_file "$auth_file"; then
|
||||
print_warn "Local legacy SSH key cleanup failed; original keys were preserved"
|
||||
print_warn "Local legacy SSH key cleanup is incomplete; check the key file and any retained staging locally"
|
||||
return 1
|
||||
fi
|
||||
print_success "Removed legacy Pulse SSH key entries from ${auth_file}"
|
||||
|
||||
Reference in New Issue
Block a user