Merge remote-tracking branch 'origin/main'

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-01 19:36:12 +01:00
4 changed files with 66 additions and 9 deletions
@@ -7331,7 +7331,8 @@ as removed, rejects that bearer thereafter, and returns the matching agent ID;
the following legacy-migration phase uses a distinct replacement enrollment
credential rather than resurrecting the removed one. Final cleanup must commit
and verify the replacement binding's removal too, then remove the fixed
root-owned helper state boundary.
root-owned helper and safe-profile transaction state boundaries through exact
path-authorized deletion.
The wrapper exercises each runtime in an isolated state root and emits the
standalone `secure-runtime-rootless-v1` receipt only after exact socket
ownership, daemon rootless attestation, installer pinning, direct telemetry,
@@ -4426,9 +4426,10 @@ redirect-denying CA/fingerprint lifecycle client before deleting services,
credentials, or recovery state. An unreachable or untrusted server is a
repair-required uninstall, not permission for local-only credential loss.
Once server removal is confirmed, full shell uninstall must also remove the
fixed root-owned privileged-helper activation/staging state. That recursive
cleanup requires an exact installer-established helper lifecycle authority;
uncertain or mismatched paths remain intact for explicit repair.
fixed root-owned privileged-helper activation/staging state and safe-profile
transaction archive. Those recursive cleanups require exact
installer-established helper and profile lifecycle authorities; uncertain or
mismatched paths remain intact for explicit repair.
TrueNAS boot recovery must follow the same rule: SCALE and CORE bootstrap
scripts may differ only in their service-manager adapter, while binary sync,
service-link recreation, and boot-time start flow stay on one installer-owned
+8
View File
@@ -195,6 +195,7 @@ TMP_ACTION_RUNNER_BIN=""
# leaves these unset and therefore never enters a migration transaction.
SAFE_PROFILE_ACTION=""
SAFE_PROFILE_STATE_DIR="/var/lib/pulse-agent-profile"
SAFE_PROFILE_STATE_DIR_REMOVAL_AUTHORITY="$SAFE_PROFILE_STATE_DIR"
SAFE_PROFILE_CURRENT_FILE="${SAFE_PROFILE_STATE_DIR}/current.env"
SAFE_PROFILE_COLLECTOR_UNIT="/etc/systemd/system/${AGENT_NAME}.service"
SAFE_PROFILE_TRANSACTION_DIR=""
@@ -3206,6 +3207,10 @@ remove_privileged_helper_state_dir() {
remove_authorized_runtime_dir "privileged helper state" "$PRIVILEGED_HELPER_STATE_DIR" "${PRIVILEGED_HELPER_STATE_DIR_REMOVAL_AUTHORITY:-}"
}
remove_safe_profile_state_dir() {
remove_authorized_runtime_dir "safe-profile state" "$SAFE_PROFILE_STATE_DIR" "${SAFE_PROFILE_STATE_DIR_REMOVAL_AUTHORITY:-}"
}
detect_qnap_data_volume() {
local qnap_vol=""
local candidate=""
@@ -5542,6 +5547,9 @@ if [[ "$UNINSTALL" == "true" ]]; then
if ! remove_privileged_helper_state_dir; then
log_warn "Retained privileged helper state at ${PRIVILEGED_HELPER_STATE_DIR}; its path was not authorized by the fixed helper lifecycle boundary."
fi
if ! remove_safe_profile_state_dir; then
log_warn "Retained safe-profile state at ${SAFE_PROFILE_STATE_DIR}; its path was not authorized by the fixed profile lifecycle boundary."
fi
# Remove least-privilege helper artifacts. The pulse-agent system user is
# deliberately left behind: deleting accounts can orphan files elsewhere,
+52 -5
View File
@@ -4055,7 +4055,53 @@ func TestInstallSHPrivilegedHelperStateRemovalRequiresExactLifecycleAuthority(t
}
}
func TestInstallSHFullUninstallRemovesPrivilegedHelperStateOnlyAfterServerConfirmation(t *testing.T) {
func TestInstallSHSafeProfileStateRemovalRequiresExactLifecycleAuthority(t *testing.T) {
for _, tc := range []struct {
name string
authority func(string) string
wantRemove bool
}{
{name: "exact fixed boundary", authority: func(path string) string { return path }, wantRemove: true},
{name: "mismatched boundary", authority: func(path string) string { return path + "-other" }, wantRemove: false},
} {
t.Run(tc.name, func(t *testing.T) {
profileState := filepath.Join(t.TempDir(), "pulse-agent-profile")
if err := os.MkdirAll(profileState, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(profileState, "current.env"), []byte("retained-or-removed"), 0o600); err != nil {
t.Fatal(err)
}
script := `
set -u
SAFE_PROFILE_STATE_DIR="` + profileState + `"
SAFE_PROFILE_STATE_DIR_REMOVAL_AUTHORITY="` + tc.authority(profileState) + `"
log_warn() { :; }
` + extractInstallShellFunction(t, "remove_authorized_runtime_dir") + `
` + extractInstallShellFunction(t, "remove_safe_profile_state_dir") + `
if remove_safe_profile_state_dir; then
printf 'removed\n'
else
printf 'retained\n'
fi
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("safe-profile-state cleanup harness: %v\n%s", err, out)
}
_, statErr := os.Stat(profileState)
if tc.wantRemove {
if !os.IsNotExist(statErr) || string(out) != "removed\n" {
t.Fatalf("authorized safe-profile state survived: stat=%v output=%q", statErr, out)
}
} else if statErr != nil || string(out) != "retained\n" {
t.Fatalf("unauthorized safe-profile state changed: stat=%v output=%q", statErr, out)
}
})
}
}
func TestInstallSHFullUninstallRemovesRuntimeStateOnlyAfterServerConfirmation(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
@@ -4063,11 +4109,12 @@ func TestInstallSHFullUninstallRemovesPrivilegedHelperStateOnlyAfterServerConfir
script := string(content)
confirmation := strings.LastIndex(script, `if ! uninstall_collector_registration; then`)
helperCleanup := strings.LastIndex(script, `if ! remove_privileged_helper_state_dir; then`)
if confirmation < 0 || helperCleanup < 0 {
t.Fatalf("full uninstall lifecycle is missing server confirmation or helper-state cleanup: confirmation=%d cleanup=%d", confirmation, helperCleanup)
profileCleanup := strings.LastIndex(script, `if ! remove_safe_profile_state_dir; then`)
if confirmation < 0 || helperCleanup < 0 || profileCleanup < 0 {
t.Fatalf("full uninstall lifecycle is missing server confirmation or fixed-state cleanup: confirmation=%d helper=%d profile=%d", confirmation, helperCleanup, profileCleanup)
}
if helperCleanup <= confirmation {
t.Fatal("privileged helper state cleanup can run before authenticated server removal")
if helperCleanup <= confirmation || profileCleanup <= confirmation {
t.Fatal("fixed privileged runtime state cleanup can run before authenticated server removal")
}
}