update security

This commit is contained in:
David Schroeder
2026-07-26 16:41:16 -05:00
parent 825a35924d
commit ebddd792b2
8 changed files with 530 additions and 4 deletions
+24
View File
@@ -16,6 +16,13 @@ interactive menu. Update availability is checked in the background and cached;
updates are installed only when explicitly selected or requested with
`tapm update`.
On a clustered Proxmox host, both `tapm update` and the management-menu update
update every online cluster node over Proxmox's root SSH trust, then update the
initiating node. Each node independently performs the same clean-worktree,
branch, and fast-forward safety checks. Offline, unreachable, dirty, ahead, or
diverged nodes are never overwritten and are reported as failures. Standalone
hosts retain the local-only update behavior.
When testing this branch, use `tapm main` to switch the installed copy back to
the published main branch. The command refuses to switch when local changes,
local-only commits, or diverged branch history would be at risk.
@@ -159,3 +166,20 @@ both values. Inputs are validated and written atomically with mode `0600`
before update checks or menu actions continue. A non-interactive launch without
valid configuration stops with an explicit setup message instead of selecting
a default company URL.
## Installation registry
V2 creates a random installation UUID and 256-bit credential in
`/var/lib/ta-proxmenu/identity.env`. The directory is mode `0700` and the file
is mode `0600`. On each interactive launch, TA-ProxMenu makes best-effort HTTPS
calls to the configured broker to record installation/upgrade state, start and
completion status, duration, TA-ProxMenu and Git versions, PVE/OS/kernel
versions, architecture, and whether the host is clustered. No background
service is installed, and broker availability never prevents the menu from
running.
The registry does not send hostnames, machine IDs, MAC addresses, usernames,
customer names, VM/container inventory, deployment codes, authorization
tokens, or command output. The broker stores only a digest of the random host
credential. A successful deployment-code exchange links the already-random
installation ID to a verified registry entry.
+3 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.7.26-5'
VERS='2026.7.26-7'
noupdate=' '
@@ -21,6 +21,8 @@ PULSE_PORT='7655'
source "${FOLDER}/inc/runtime-config.inc"
TAPM_ENSURE_RUNTIME_CONFIG || return 1 2>/dev/null || exit 1
source "${FOLDER}/inc/fleet.inc"
TAPM_FLEET_ENSURE_IDENTITY || true
S1_BROKER_PACKAGE='sentinelone-linux'
S1_PACKAGE='tapm-sentinelone.deb'
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# Cluster discovery and remote update helpers for TA-ProxMenu.
TAPM_COROSYNC_CONFIG="${TAPM_COROSYNC_CONFIG:-/etc/pve/corosync.conf}"
TAPM_CLUSTER_NODES_FROM_JSON() {
python3 -c '
import json, re, sys
try:
payload = json.load(sys.stdin)
except (json.JSONDecodeError, OSError):
raise SystemExit(1)
if isinstance(payload, dict):
payload = payload.get("data", [])
if not isinstance(payload, list):
raise SystemExit(1)
valid_name = re.compile(r"^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$")
rows = []
for item in payload:
if not isinstance(item, dict):
continue
name = str(item.get("node", "")).strip()
status = str(item.get("status", "unknown")).strip().lower()
if valid_name.fullmatch(name):
rows.append((name, status))
for name, status in sorted(rows):
print(f"{name}\t{status}")
'
}
TAPM_CLUSTER_NODE_ROWS() {
local node_json
node_json="$(timeout 10 pvesh get /nodes --output-format json 2>/dev/null)" ||
return 1
printf '%s' "$node_json" | TAPM_CLUSTER_NODES_FROM_JSON
}
TAPM_LOCAL_CLUSTER_NODE() {
local local_link=''
local_link="$(readlink /etc/pve/local 2>/dev/null || true)"
if [[ -n "$local_link" ]]; then
basename "$local_link"
else
hostname -s
fi
}
TAPM_UPDATE_REMOTE_NODE() {
local node="$1"
[[ "$node" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || return 1
timeout 240 ssh \
-o BatchMode=yes \
-o ConnectTimeout=10 \
-o ServerAliveInterval=15 \
-o ServerAliveCountMax=2 \
"root@${node}" \
'/opt/idssys/ta-proxmenu/run.sh update --local-only'
}
INSTALL_CLUSTER_UPDATES() {
local index
local local_node
local node
local required_command
local status
local node_rows=''
local cluster_failed=0
local -a cluster_nodes=()
local -a cluster_statuses=()
if [[ ! -s "$TAPM_COROSYNC_CONFIG" ]]; then
INSTALL_LOCAL_UPDATES
return $?
fi
for required_command in pvesh python3 ssh timeout; do
if ! command -v "$required_command" >/dev/null 2>&1; then
echo -e "${idsCL[Red]}Cluster update requires ${required_command}; no nodes were updated.${idsCL[Default]}"
return 1
fi
done
node_rows="$(TAPM_CLUSTER_NODE_ROWS)" || {
echo -e "${idsCL[Red]}Could not retrieve the Proxmox cluster node list; no nodes were updated.${idsCL[Default]}"
return 1
}
while IFS=$'\t' read -r node status; do
[[ -n "$node" ]] || continue
cluster_nodes+=("$node")
cluster_statuses+=("$status")
done <<<"$node_rows"
if (( ${#cluster_nodes[@]} == 0 )); then
echo -e "${idsCL[Red]}The Proxmox API returned no cluster nodes; no nodes were updated.${idsCL[Default]}"
return 1
fi
local_node="$(TAPM_LOCAL_CLUSTER_NODE)"
echo -e "${idsCL[LightCyan]}Updating TA-ProxMenu across ${#cluster_nodes[@]} cluster node(s)...${idsCL[Default]}"
for index in "${!cluster_nodes[@]}"; do
node="${cluster_nodes[$index]}"
status="${cluster_statuses[$index]}"
if [[ "$node" == "$local_node" ]]; then
continue
fi
if [[ "$status" != "online" ]]; then
echo -e "${idsCL[LightYellow]}Skipping ${node}: node status is ${status}.${idsCL[Default]}"
cluster_failed=1
continue
fi
echo -e "${idsCL[LightCyan]}Updating remote node ${node}...${idsCL[Default]}"
if TAPM_UPDATE_REMOTE_NODE "$node"; then
echo -e "${idsCL[Green]}Remote node ${node} is updated.${idsCL[Default]}"
else
echo -e "${idsCL[Red]}Remote node ${node} failed to update.${idsCL[Default]}"
cluster_failed=1
fi
done
echo -e "${idsCL[LightCyan]}Updating local node ${local_node}...${idsCL[Default]}"
INSTALL_LOCAL_UPDATES || cluster_failed=1
if (( cluster_failed == 0 )); then
echo -e "${idsCL[Green]}TA-ProxMenu is updated on all ${#cluster_nodes[@]} cluster node(s).${idsCL[Default]}"
return 0
fi
echo -e "${idsCL[LightYellow]}Cluster update completed with failures; review the node messages above.${idsCL[Default]}"
return 1
}
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
TAPM_FLEET_STATE_DIR="${TAPM_FLEET_STATE_DIR:-/var/lib/ta-proxmenu}"
TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_IDENTITY_FILE:-${TAPM_FLEET_STATE_DIR}/identity.env}"
TAPM_FLEET_INSTALLATION_ID=''
TAPM_FLEET_CREDENTIAL=''
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_STARTED_AT=0
TAPM_FLEET_VERSION=''
TAPM_FLEET_READ_VALUE() {
local key="$1"
local value=''
if [[ -r "$TAPM_FLEET_IDENTITY_FILE" ]]; then
value="$(
sed -n "s/^${key}=//p" "$TAPM_FLEET_IDENTITY_FILE" |
tail -n 1
)"
fi
printf '%s' "$value"
}
TAPM_FLEET_LOAD_IDENTITY() {
TAPM_FLEET_INSTALLATION_ID="$(TAPM_FLEET_READ_VALUE INSTALLATION_ID)"
TAPM_FLEET_CREDENTIAL="$(TAPM_FLEET_READ_VALUE CREDENTIAL)"
TAPM_FLEET_LAST_VERSION="$(TAPM_FLEET_READ_VALUE LAST_VERSION)"
}
TAPM_FLEET_VALID_IDENTITY() {
[[ "$TAPM_FLEET_INSTALLATION_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ &&
"$TAPM_FLEET_CREDENTIAL" =~ ^[0-9a-f]{64}$ ]]
}
TAPM_FLEET_WRITE_IDENTITY() {
local temporary_file
mkdir -p "$TAPM_FLEET_STATE_DIR" 2>/dev/null || return 1
chmod 0700 "$TAPM_FLEET_STATE_DIR" 2>/dev/null || return 1
temporary_file="$(mktemp "${TAPM_FLEET_IDENTITY_FILE}.tmp.XXXXXX")" || return 1
if ! {
printf 'INSTALLATION_ID=%s\n' "$TAPM_FLEET_INSTALLATION_ID"
printf 'CREDENTIAL=%s\n' "$TAPM_FLEET_CREDENTIAL"
printf 'LAST_VERSION=%s\n' "$TAPM_FLEET_LAST_VERSION"
} >"$temporary_file" ||
! chmod 0600 "$temporary_file" ||
! mv -f "$temporary_file" "$TAPM_FLEET_IDENTITY_FILE"; then
rm -f "$temporary_file"
return 1
fi
}
TAPM_FLEET_ENSURE_IDENTITY() {
TAPM_FLEET_LOAD_IDENTITY
TAPM_FLEET_VALID_IDENTITY && return 0
command -v openssl >/dev/null 2>&1 || return 1
if [[ -r /proc/sys/kernel/random/uuid ]]; then
IFS= read -r TAPM_FLEET_INSTALLATION_ID </proc/sys/kernel/random/uuid
elif command -v uuidgen >/dev/null 2>&1; then
TAPM_FLEET_INSTALLATION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
else
return 1
fi
TAPM_FLEET_CREDENTIAL="$(openssl rand -hex 32)" || return 1
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_WRITE_IDENTITY
}
TAPM_FLEET_COLLECT_METADATA() {
TAPM_FLEET_GIT_COMMIT="$(git -C "${FOLDER:-/opt/idssys/ta-proxmenu}" rev-parse HEAD 2>/dev/null || true)"
TAPM_FLEET_PVE_VERSION="$(pveversion 2>/dev/null | head -n 1 || true)"
TAPM_FLEET_OS_VERSION="$(
if [[ -r /etc/os-release ]]; then
(
# shellcheck disable=SC1091
source /etc/os-release
printf '%s' "${PRETTY_NAME:-}"
)
fi
)"
TAPM_FLEET_KERNEL_VERSION="$(uname -r 2>/dev/null || true)"
TAPM_FLEET_ARCHITECTURE="$(
dpkg --print-architecture 2>/dev/null ||
uname -m 2>/dev/null ||
true
)"
if [[ -s /etc/pve/corosync.conf ]]; then
TAPM_FLEET_CLUSTERED=true
else
TAPM_FLEET_CLUSTERED=false
fi
}
TAPM_FLEET_JSON() {
local event="$1"
local result="$2"
local error_code="${3:-}"
local duration="${4:-0}"
TAPM_FLEET_EVENT="$event" \
TAPM_FLEET_RESULT="$result" \
TAPM_FLEET_ERROR_CODE="$error_code" \
TAPM_FLEET_DURATION="$duration" \
TAPM_FLEET_INSTALLATION_ID="$TAPM_FLEET_INSTALLATION_ID" \
TAPM_FLEET_CREDENTIAL="$TAPM_FLEET_CREDENTIAL" \
TAPM_FLEET_VERSION="$TAPM_FLEET_VERSION" \
TAPM_FLEET_GIT_COMMIT="$TAPM_FLEET_GIT_COMMIT" \
TAPM_FLEET_PVE_VERSION="$TAPM_FLEET_PVE_VERSION" \
TAPM_FLEET_OS_VERSION="$TAPM_FLEET_OS_VERSION" \
TAPM_FLEET_KERNEL_VERSION="$TAPM_FLEET_KERNEL_VERSION" \
TAPM_FLEET_ARCHITECTURE="$TAPM_FLEET_ARCHITECTURE" \
TAPM_FLEET_CLUSTERED="$TAPM_FLEET_CLUSTERED" \
TAPM_FLEET_INCLUDE_CREDENTIAL="${TAPM_FLEET_INCLUDE_CREDENTIAL:-0}" \
python3 -c '
import json, os, sys
payload = {
"schema_version": 1,
"installation_id": os.environ["TAPM_FLEET_INSTALLATION_ID"],
"event": os.environ["TAPM_FLEET_EVENT"],
"result": os.environ["TAPM_FLEET_RESULT"],
"proxmenu_version": os.environ["TAPM_FLEET_VERSION"],
"git_commit": os.environ["TAPM_FLEET_GIT_COMMIT"],
"pve_version": os.environ["TAPM_FLEET_PVE_VERSION"],
"os_version": os.environ["TAPM_FLEET_OS_VERSION"],
"kernel_version": os.environ["TAPM_FLEET_KERNEL_VERSION"],
"architecture": os.environ["TAPM_FLEET_ARCHITECTURE"],
"clustered": os.environ["TAPM_FLEET_CLUSTERED"] == "true",
"error_code": os.environ["TAPM_FLEET_ERROR_CODE"],
"duration_seconds": int(os.environ["TAPM_FLEET_DURATION"]),
}
if os.environ.get("TAPM_FLEET_INCLUDE_CREDENTIAL") == "1":
payload["credential"] = os.environ["TAPM_FLEET_CREDENTIAL"]
json.dump(payload, sys.stdout, separators=(",", ":"))
'
}
TAPM_FLEET_REGISTER() {
TAPM_FLEET_INCLUDE_CREDENTIAL=1 TAPM_FLEET_JSON installed success |
curl --fail --silent --show-error \
--connect-timeout 3 --max-time 10 \
--header 'Content-Type: application/json' \
--data-binary @- \
"${TAPM_BROKER_URL}/api/v1/hosts/register" \
>/dev/null 2>&1
}
TAPM_FLEET_EVENT_SEND() {
local event="$1"
local result="$2"
local error_code="${3:-}"
local duration="${4:-0}"
local event_payload=''
(( TAPM_FLEET_REGISTERED == 1 )) || return 0
event_payload="$(mktemp "${TMPDIR:-/tmp}/tapm-fleet-event.XXXXXX")" || return 0
chmod 0600 "$event_payload" 2>/dev/null || {
rm -f "$event_payload"
return 0
}
if ! TAPM_FLEET_JSON "$event" "$result" "$error_code" "$duration" >"$event_payload"; then
rm -f "$event_payload"
return 0
fi
printf 'header = "Content-Type: application/json"\nheader = "Authorization: Bearer %s"\nurl = "%s/api/v1/hosts/events"\n' \
"$TAPM_FLEET_CREDENTIAL" "$TAPM_BROKER_URL" |
curl --fail --silent --show-error \
--connect-timeout 3 --max-time 10 \
--config - --data-binary "@${event_payload}" \
>/dev/null 2>&1 || true
rm -f "$event_payload"
}
TAPM_FLEET_START() {
TAPM_FLEET_VERSION="$1"
TAPM_FLEET_STARTED_AT="$(date +%s)"
TAPM_FLEET_ENSURE_IDENTITY || return 0
command -v python3 >/dev/null 2>&1 || return 0
command -v curl >/dev/null 2>&1 || return 0
TAPM_FLEET_COLLECT_METADATA
if TAPM_FLEET_REGISTER; then
TAPM_FLEET_REGISTERED=1
else
return 0
fi
if [[ -n "$TAPM_FLEET_LAST_VERSION" &&
"$TAPM_FLEET_LAST_VERSION" != "$TAPM_FLEET_VERSION" ]]; then
TAPM_FLEET_EVENT_SEND upgraded success
fi
TAPM_FLEET_EVENT_SEND run_started started
}
TAPM_FLEET_FINISH() {
local exit_status="${1:-0}"
local duration=0
if [[ "$TAPM_FLEET_STARTED_AT" =~ ^[0-9]+$ ]] &&
(( TAPM_FLEET_STARTED_AT > 0 )); then
duration="$(( $(date +%s) - TAPM_FLEET_STARTED_AT ))"
fi
if (( exit_status == 0 )); then
TAPM_FLEET_EVENT_SEND run_completed success '' "$duration"
else
TAPM_FLEET_EVENT_SEND run_failed failure exit_nonzero "$duration"
fi
if (( TAPM_FLEET_REGISTERED == 1 )) && TAPM_FLEET_VALID_IDENTITY; then
TAPM_FLEET_LAST_VERSION="$TAPM_FLEET_VERSION"
TAPM_FLEET_WRITE_IDENTITY || true
fi
return 0
}
+2 -1
View File
@@ -185,8 +185,9 @@ TAPM_AUTHORIZE() {
exchange_response="$(
TAPM_CODE="$deploycode" TAPM_FINGERPRINT="$host_fingerprint" TAPM_HOSTNAME="$(hostname)" \
TAPM_LAN_IP="${RNIP:-}" \
TAPM_INSTALLATION_ID="${TAPM_FLEET_INSTALLATION_ID:-}" \
TAPM_REQUESTED_ACTION="$required_action" TAPM_REQUESTED_PACKAGE="$required_package" \
python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "lan_ip": os.environ["TAPM_LAN_IP"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' |
python3 -c 'import json, os, sys; json.dump({"code": os.environ["TAPM_CODE"], "host_fingerprint": os.environ["TAPM_FINGERPRINT"], "hostname": os.environ["TAPM_HOSTNAME"], "lan_ip": os.environ["TAPM_LAN_IP"], "installation_id": os.environ["TAPM_INSTALLATION_ID"], "requested_action": os.environ["TAPM_REQUESTED_ACTION"], "requested_package": os.environ["TAPM_REQUESTED_PACKAGE"]}, sys.stdout)' |
curl --fail --silent --show-error \
--header 'Content-Type: application/json' \
--data-binary @- "${TAPM_BROKER_URL}/api/v1/exchange"
+9 -2
View File
@@ -10,6 +10,7 @@ TAPM_ENSURE_RUNTIME_CONFIG || exit 1
DEFAULTS_REPOSITORY_URL="${GITEA_URL}/voltron/iDS-Defaults.git"
source /opt/idssys/ta-proxmenu/inc/git-update.inc
source /opt/idssys/ta-proxmenu/inc/cluster-update.inc
AUTO_UPDATE_DEFAULTS() {
local checked_at=0
@@ -100,6 +101,8 @@ AUTO_UPDATE_DEFAULTS
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
source /opt/idssys/defaults/default.inc
source /opt/idssys/ta-proxmenu/defaults.inc
TAPM_FLEET_START "$VERS"
trap 'TAPM_FLEET_FINISH "$?"' EXIT
UPDATE_REPOSITORY() {
local repository="$1"
@@ -235,7 +238,7 @@ SWITCH_TAPM_BRANCH() {
exec /opt/idssys/ta-proxmenu/run.sh
}
INSTALL_UPDATES() {
INSTALL_LOCAL_UPDATES() {
local current_branch
local defaults_warning=0
local update_failed=0
@@ -279,7 +282,11 @@ INSTALL_UPDATES() {
case "${1:-}" in
update|u)
INSTALL_UPDATES
if [[ "${2:-}" == "--local-only" ]]; then
INSTALL_LOCAL_UPDATES
else
INSTALL_CLUSTER_UPDATES
fi
exit $?
;;
main)
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/cluster-update.inc"
nodes_json='[
{"node":"pve3","status":"offline"},
{"node":"pve1","status":"online"},
{"node":"pve2","status":"online"},
{"node":"bad node","status":"online"}
]'
expected=$'pve1\tonline\npve2\tonline\npve3\toffline'
assert_equal "$expected" \
"$(printf '%s' "$nodes_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
"cluster node discovery filters and sorts API output"
wrapped_json='{"data":[{"node":"pve2.example","status":"ONLINE"}]}'
assert_equal $'pve2.example\tonline' \
"$(printf '%s' "$wrapped_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
"wrapped API response and normalized status"
if printf '%s' 'not-json' | TAPM_CLUSTER_NODES_FROM_JSON 2>/dev/null; then
printf 'FAIL: malformed cluster JSON was accepted\n' >&2
exit 1
fi
assert_failure "remote node rejects shell characters" \
TAPM_UPDATE_REMOTE_NODE 'pve1;reboot'
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
TAPM_COROSYNC_CONFIG="${test_dir}/corosync.conf"
printf 'totem {}\n' >"$TAPM_COROSYNC_CONFIG"
update_log="${test_dir}/updates"
LightCyan=0
LightYellow=0
Green=0
Red=0
Default=0
idsCL[0]=''
pvesh() {
return 0
}
TAPM_CLUSTER_NODE_ROWS() {
printf 'pve1\tonline\npve2\tonline\npve3\toffline\n'
}
TAPM_LOCAL_CLUSTER_NODE() {
printf 'pve1\n'
}
TAPM_UPDATE_REMOTE_NODE() {
printf 'remote:%s\n' "$1" >>"$update_log"
}
INSTALL_LOCAL_UPDATES() {
printf 'local:pve1\n' >>"$update_log"
}
if INSTALL_CLUSTER_UPDATES >/dev/null; then
printf 'FAIL: cluster update ignored an offline node\n' >&2
exit 1
fi
assert_equal $'remote:pve2\nlocal:pve1' \
"$(cat "$update_log")" \
"online remotes and initiating node update once"
finish_tests
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
TAPM_FLEET_STATE_DIR="${test_dir}/state"
TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_STATE_DIR}/identity.env"
TAPM_BROKER_URL='https://tapm.example.com'
source "${TEST_ROOT}/inc/fleet.inc"
TAPM_FLEET_ENSURE_IDENTITY
if ! TAPM_FLEET_VALID_IDENTITY; then
printf 'FAIL: generated fleet identity is invalid\n' >&2
exit 1
fi
if stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE" >/dev/null 2>&1; then
identity_mode="$(stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE")"
else
identity_mode="$(stat -f '%Lp' "$TAPM_FLEET_IDENTITY_FILE")"
fi
if [[ "$identity_mode" != 600 ]]; then
printf 'FAIL: fleet identity mode is not 600\n' >&2
exit 1
fi
TAPM_FLEET_VERSION='2026.7.26-7'
TAPM_FLEET_GIT_COMMIT='0123456789abcdef0123456789abcdef01234567'
TAPM_FLEET_PVE_VERSION='pve-manager/9.0.3/abc~1'
TAPM_FLEET_OS_VERSION='Debian GNU/Linux 13 (trixie)'
TAPM_FLEET_KERNEL_VERSION='6.14.11-2-pve'
TAPM_FLEET_ARCHITECTURE='amd64'
TAPM_FLEET_CLUSTERED=true
registration_json="$(
TAPM_FLEET_INCLUDE_CREDENTIAL=1 TAPM_FLEET_JSON installed success
)"
REGISTRATION_JSON="$registration_json" python3 -c '
import json, os
payload = json.loads(os.environ["REGISTRATION_JSON"])
assert payload["installation_id"]
assert len(payload["credential"]) == 64
assert payload["proxmenu_version"] == "2026.7.26-7"
assert payload["clustered"] is True
'
printf '0\n' >"${test_dir}/curl-count"
curl() {
local count data_path='' previous='' argument=''
count="$(cat "${test_dir}/curl-count")"
count="$((count + 1))"
printf '%s\n' "$count" >"${test_dir}/curl-count"
for argument in "$@"; do
if [[ "$previous" == '--data-binary' ]]; then
data_path="${argument#@}"
fi
previous="$argument"
done
if [[ -n "$data_path" ]]; then
cp "$data_path" "${test_dir}/curl-body-${count}.json"
fi
cat >"${test_dir}/curl-config-${count}"
return 0
}
TAPM_FLEET_REGISTERED=1
TAPM_FLEET_EVENT_SEND run_completed success '' 12
EVENT_BODY="$(cat "${test_dir}/curl-body-1.json")" \
EVENT_CONFIG="$(cat "${test_dir}/curl-config-1")" \
EXPECTED_CREDENTIAL="$TAPM_FLEET_CREDENTIAL" \
python3 -c '
import json, os
payload = json.loads(os.environ["EVENT_BODY"])
assert payload["event"] == "run_completed"
assert payload["result"] == "success"
assert payload["duration_seconds"] == 12
assert "credential" not in payload
config = os.environ["EVENT_CONFIG"]
assert "Authorization: Bearer " + os.environ["EXPECTED_CREDENTIAL"] in config
assert "https://tapm.example.com/api/v1/hosts/events" in config
'
if compgen -G "${TMPDIR:-/tmp}/tapm-fleet-event.*" >/dev/null; then
printf 'FAIL: fleet event left a temporary payload file\n' >&2
exit 1
fi
printf 'PASS: fleet identity and event client\n'