chore: snapshot current changes

This commit is contained in:
rcourtman 2025-11-02 22:47:55 +00:00
parent fb22469eb0
commit 5c4be1921c
38 changed files with 11149 additions and 4102 deletions

View file

@ -48,10 +48,25 @@ export FRONTEND_DEV_HOST FRONTEND_DEV_PORT
export PULSE_DEV_API_HOST PULSE_DEV_API_PORT PULSE_DEV_API_URL PULSE_DEV_WS_URL
# Auto-detect pulse-sensor-proxy socket if available
HOST_PROXY_SOCKET="/mnt/pulse-proxy/pulse-sensor-proxy.sock"
CONTAINER_PROXY_SOCKET="/run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
if [[ -z ${PULSE_SENSOR_PROXY_SOCKET:-} ]]; then
if [[ -S /mnt/pulse-proxy/pulse-sensor-proxy.sock ]]; then
export PULSE_SENSOR_PROXY_SOCKET=/mnt/pulse-proxy/pulse-sensor-proxy.sock
if [[ -S "${HOST_PROXY_SOCKET}" ]]; then
export PULSE_SENSOR_PROXY_SOCKET="${HOST_PROXY_SOCKET}"
printf "[hot-dev] Detected pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
elif [[ -S "${CONTAINER_PROXY_SOCKET}" ]]; then
export PULSE_SENSOR_PROXY_SOCKET="${CONTAINER_PROXY_SOCKET}"
printf "[hot-dev] WARNING: Using container-local pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
printf "[hot-dev] WARNING: Host proxy is missing; temperatures will not reach Pulse until it is reinstalled.\n"
else
printf "[hot-dev] WARNING: No pulse-sensor-proxy socket detected. Temperatures will be unavailable.\n"
fi
else
if [[ ! -S "${PULSE_SENSOR_PROXY_SOCKET}" ]]; then
printf "[hot-dev] WARNING: Configured pulse-sensor-proxy socket not found at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
elif [[ "${PULSE_SENSOR_PROXY_SOCKET}" == "${CONTAINER_PROXY_SOCKET}" && ! -S "${HOST_PROXY_SOCKET}" ]]; then
printf "[hot-dev] WARNING: Using container-local proxy socket; reinstall host pulse-sensor-proxy for real telemetry.\n"
fi
fi

View file

@ -697,6 +697,7 @@ download_agent_binary() {
fi
if http::download "${download_args[@]}"; then
AGENT_DOWNLOAD_SOURCE="${primary_url}"
return 0
fi
@ -711,6 +712,7 @@ download_agent_binary() {
download_args+=(--insecure)
fi
if http::download "${download_args[@]}"; then
AGENT_DOWNLOAD_SOURCE="${fallback_url}"
return 0
fi
@ -722,6 +724,8 @@ download_agent_binary() {
return 1
}
unset AGENT_DOWNLOAD_SOURCE
if download_agent_binary "$DOWNLOAD_URL" "$DOWNLOAD_URL_BASE"; then
:
else
@ -730,6 +734,103 @@ else
exit 1
fi
fetch_checksum_header() {
local url="$1"
local header=""
if command -v curl &> /dev/null; then
local curl_args=(-fsSI "$url")
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
curl_args=(-k "${curl_args[@]}")
fi
header=$(curl "${curl_args[@]}" 2>/dev/null || true)
elif command -v wget &> /dev/null; then
local tmp
tmp=$(mktemp)
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
wget --spider --no-check-certificate --server-response "$url" >/dev/null 2>"$tmp" || true
else
wget --spider --server-response "$url" >/dev/null 2>"$tmp" || true
fi
header=$(cat "$tmp" 2>/dev/null || true)
rm -f "$tmp"
fi
if [[ -z "$header" ]]; then
return 1
fi
local checksum_line
checksum_line=$(printf '%s\n' "$header" | awk 'BEGIN{IGNORECASE=1} /^ *X-Checksum-Sha256:/{print $0; exit}')
if [[ -z "$checksum_line" ]]; then
return 1
fi
local value
value=$(printf '%s\n' "$checksum_line" | awk -F':' '{sub(/^[[:space:]]*/,"",$2); print $2}')
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
if [[ -z "$value" ]]; then
return 1
fi
FETCHED_CHECKSUM="$value"
return 0
}
calculate_sha256() {
local file="$1"
local hash=""
if command -v sha256sum &> /dev/null; then
hash=$(sha256sum "$file" | awk '{print $1}')
elif command -v shasum &> /dev/null; then
hash=$(shasum -a 256 "$file" | awk '{print $1}')
fi
if [[ -z "$hash" ]]; then
return 1
fi
CALCULATED_CHECKSUM=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]')
return 0
}
verify_agent_checksum() {
local url="$1"
if common::is_dry_run; then
log_info '[dry-run] Skipping checksum verification'
return 0
fi
if ! fetch_checksum_header "$url"; then
log_warn 'Agent download did not include X-Checksum-Sha256 header; skipping verification'
return 0
fi
if ! calculate_sha256 "$AGENT_PATH"; then
log_warn 'Unable to calculate sha256 checksum locally; skipping verification'
return 0
fi
if [[ "$FETCHED_CHECKSUM" != "$CALCULATED_CHECKSUM" ]]; then
rm -f "$AGENT_PATH"
log_error "Checksum mismatch. Expected $FETCHED_CHECKSUM but downloaded $CALCULATED_CHECKSUM"
return 1
fi
log_success 'Checksum verified for agent binary'
unset FETCHED_CHECKSUM CALCULATED_CHECKSUM
return 0
}
if [[ -n "${AGENT_DOWNLOAD_SOURCE:-}" ]]; then
if ! verify_agent_checksum "$AGENT_DOWNLOAD_SOURCE"; then
log_error 'Agent download failed checksum verification'
exit 1
fi
fi
if ! common::is_dry_run; then
chmod +x "$AGENT_PATH"
fi
@ -841,8 +942,8 @@ SYSTEMD_ENV_INSECURE_LINE="Environment=\"PULSE_INSECURE_SKIP_VERIFY=$PRIMARY_INS
systemd::create_service "$SERVICE_PATH" <<EOF
[Unit]
Description=Pulse Docker Agent
After=network-online.target docker.service
Wants=network-online.target
After=network-online.target docker.socket docker.service
Wants=network-online.target docker.socket
[Service]
Type=simple
@ -854,6 +955,22 @@ ExecStart=$AGENT_PATH --url "$PRIMARY_URL" --interval "$INTERVAL"$NO_AUTO_UPDATE
Restart=on-failure
RestartSec=5s
User=root
ProtectSystem=full
ProtectHome=read-only
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
UMask=0077
NoNewPrivileges=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
PrivateTmp=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/run/docker.sock
ProtectHostname=yes
ProtectClock=yes
[Install]
WantedBy=multi-user.target

View file

@ -343,6 +343,193 @@ resolve_agent_path_for_uninstall() {
printf '%s' "$DEFAULT_AGENT_PATH"
}
ensure_service_user() {
SERVICE_USER_ACTUAL="$SERVICE_USER"
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
SERVICE_USER_AVAILABLE="true"
if [[ "$SERVICE_USER" == "root" ]]; then
SERVICE_GROUP_ACTUAL="root"
SERVICE_USER_AVAILABLE="false"
return
fi
if id -u "$SERVICE_USER" >/dev/null 2>&1; then
if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
else
SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")"
fi
return
fi
if command -v useradd >/dev/null 2>&1; then
if useradd --system --home-dir "$SERVICE_HOME" --shell /usr/sbin/nologin "$SERVICE_USER" >/dev/null 2>&1; then
SERVICE_USER_CREATED="true"
fi
elif command -v adduser >/dev/null 2>&1; then
if adduser --system --home "$SERVICE_HOME" --shell /usr/sbin/nologin "$SERVICE_USER" >/dev/null 2>&1; then
SERVICE_USER_CREATED="true"
fi
else
log_warn "Unable to create dedicated service user; running agent as root"
SERVICE_USER_ACTUAL="root"
SERVICE_GROUP_ACTUAL="root"
SERVICE_USER_AVAILABLE="false"
return
fi
if id -u "$SERVICE_USER" >/dev/null 2>&1; then
SERVICE_USER_ACTUAL="$SERVICE_USER"
if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
else
SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")"
fi
if [[ "$SERVICE_USER_CREATED" == "true" ]]; then
log_success "Created service user: $SERVICE_USER"
fi
return
fi
log_warn "Failed to create service user; falling back to root"
SERVICE_USER_ACTUAL="root"
SERVICE_GROUP_ACTUAL="root"
SERVICE_USER_AVAILABLE="false"
}
ensure_service_home() {
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
return
fi
if [[ -z "$SERVICE_HOME" ]]; then
return
fi
if [[ ! -d "$SERVICE_HOME" ]]; then
mkdir -p "$SERVICE_HOME"
fi
chown "$SERVICE_USER_ACTUAL":"$SERVICE_GROUP_ACTUAL" "$SERVICE_HOME" >/dev/null 2>&1 || true
chmod 750 "$SERVICE_HOME" >/dev/null 2>&1 || true
}
ensure_docker_group_membership() {
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE=""
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
return
fi
if getent group docker >/dev/null 2>&1; then
DOCKER_GROUP_PRESENT="true"
if ! id -nG "$SERVICE_USER_ACTUAL" 2>/dev/null | tr ' ' '\n' | grep -Fxq "docker"; then
if command -v usermod >/dev/null 2>&1; then
usermod -a -G docker "$SERVICE_USER_ACTUAL" >/dev/null 2>&1 || log_warn "Failed to add $SERVICE_USER_ACTUAL to docker group; adjust socket permissions manually."
elif command -v adduser >/dev/null 2>&1; then
adduser "$SERVICE_USER_ACTUAL" docker >/dev/null 2>&1 || log_warn "Failed to add $SERVICE_USER_ACTUAL to docker group; adjust socket permissions manually."
else
log_warn "Unable to manage docker group membership; ensure $SERVICE_USER_ACTUAL can access /var/run/docker.sock"
fi
fi
if id -nG "$SERVICE_USER_ACTUAL" 2>/dev/null | tr ' ' '\n' | grep -Fxq "docker"; then
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE="SupplementaryGroups=docker"
log_success "Ensured docker group access for $SERVICE_USER_ACTUAL"
else
log_warn "Service user $SERVICE_USER_ACTUAL is not in docker group; ensure the Docker socket ACL grants access."
fi
else
log_warn "docker group not found; ensure the agent user can access /var/run/docker.sock"
fi
}
write_env_file() {
local target="$ENV_FILE"
local dir
dir=$(dirname "$target")
mkdir -p "$dir"
local tmp
tmp=$(mktemp "${target}.XXXXXX")
chmod 600 "$tmp"
{
if [[ -n "$PRIMARY_URL" ]]; then
printf 'PULSE_URL=%q\n' "$PRIMARY_URL"
fi
if [[ -n "$PRIMARY_TOKEN" ]]; then
printf 'PULSE_TOKEN=%q\n' "$PRIMARY_TOKEN"
fi
if [[ -n "$JOINED_TARGETS" ]]; then
printf 'PULSE_TARGETS=%q\n' "$JOINED_TARGETS"
fi
if [[ -n "$PRIMARY_INSECURE" ]]; then
printf 'PULSE_INSECURE_SKIP_VERIFY=%q\n' "$PRIMARY_INSECURE"
fi
if [[ -n "$INTERVAL" ]]; then
printf 'PULSE_INTERVAL=%q\n' "$INTERVAL"
fi
if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then
printf 'PULSE_NO_AUTO_UPDATE=true\n'
fi
} > "$tmp"
chown root:root "$tmp"
chmod 600 "$tmp"
mv "$tmp" "$target"
log_success "Wrote environment file: $target"
}
configure_polkit_rule() {
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
return
fi
local polkit_dir="/etc/polkit-1/rules.d"
if [[ ! -d "$polkit_dir" ]]; then
log_warn "polkit not detected; remote stop commands may require manual sudo access"
return
fi
local rule_path="${polkit_dir}/90-pulse-docker-agent.rules"
local tmp
tmp=$(mktemp "${rule_path}.XXXXXX")
cat > "$tmp" <<EOF
// Pulse Docker agent installer managed rule
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.systemd1.manage-units" ||
action.id == "org.freedesktop.systemd1.manage-unit-files") &&
subject.user == "$SERVICE_USER_ACTUAL") {
return polkit.Result.YES;
}
});
EOF
chown root:root "$tmp" 2>/dev/null || true
chmod 0644 "$tmp"
if [[ -f "$rule_path" ]]; then
if command -v cmp >/dev/null 2>&1 && cmp -s "$tmp" "$rule_path" 2>/dev/null; then
rm -f "$tmp"
log_info "polkit rule already present for $SERVICE_USER_ACTUAL"
return
fi
fi
mv "$tmp" "$rule_path"
log_success "Configured polkit rule allowing $SERVICE_USER_ACTUAL to manage pulse-docker-agent service"
}
remove_polkit_rule() {
local polkit_dir="/etc/polkit-1/rules.d"
local rule_path="${polkit_dir}/90-pulse-docker-agent.rules"
if [[ -f "$rule_path" ]] && grep -q 'Pulse Docker agent installer managed rule' "$rule_path" 2>/dev/null; then
rm -f "$rule_path"
log_success "Removed polkit rule: $rule_path"
fi
}
# Pulse Docker Agent Installer/Uninstaller
# Install (single target):
# curl -fsSL http://pulse.example.com/install-docker-agent.sh | bash -s -- --url http://pulse.example.com --token <api-token>
@ -370,6 +557,7 @@ DEFAULT_AGENT_PATH_WRITABLE="unknown"
EXISTING_AGENT_PATH=""
AGENT_PATH=""
SERVICE_PATH="/etc/systemd/system/pulse-docker-agent.service"
ENV_FILE="/etc/pulse/pulse-docker-agent.env"
UNRAID_STARTUP="/boot/config/go.d/pulse-docker-agent.sh"
LOG_PATH="/var/log/pulse-docker-agent.log"
INTERVAL="30s"
@ -385,6 +573,15 @@ PRIMARY_TOKEN=""
PRIMARY_INSECURE="false"
JOINED_TARGETS=""
ORIGINAL_ARGS=("$@")
SERVICE_USER="pulse-docker"
SERVICE_GROUP="$SERVICE_USER"
SERVICE_HOME="/var/lib/pulse-docker-agent"
SERVICE_USER_ACTUAL="$SERVICE_USER"
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
SERVICE_USER_CREATED="false"
SERVICE_USER_AVAILABLE="true"
DOCKER_GROUP_PRESENT="false"
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
@ -687,6 +884,11 @@ if [ "$UNINSTALL" = true ]; then
log_warn "systemctl not found; skipping service disable"
fi
if [ -f "$ENV_FILE" ]; then
rm -f "$ENV_FILE"
log_success "Removed environment file: $ENV_FILE"
fi
if pgrep -f pulse-docker-agent > /dev/null 2>&1; then
log_info "Stopping running agent processes"
pkill -f pulse-docker-agent 2>/dev/null || true
@ -705,6 +907,8 @@ if [ "$UNINSTALL" = true ]; then
log_success "Removed Unraid startup script: $UNRAID_STARTUP"
fi
remove_polkit_rule
if [ "$PURGE" = true ]; then
if [ -f "$LOG_PATH" ]; then
rm -f "$LOG_PATH"
@ -712,6 +916,10 @@ if [ "$UNINSTALL" = true ]; then
else
log_info "Agent log file already absent: $LOG_PATH"
fi
if [[ -d "$SERVICE_HOME" ]]; then
rm -rf "$SERVICE_HOME"
log_success "Removed service home directory: $SERVICE_HOME"
fi
elif [ -f "$LOG_PATH" ]; then
log_info "Preserving agent log file at $LOG_PATH (use --purge to remove)"
fi
@ -946,16 +1154,109 @@ download_agent_from_url() {
return 1
}
fetch_checksum_header() {
local url="$1"
local header=""
if command -v curl &> /dev/null; then
local curl_args=(-fsSI "$url")
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
curl_args=(-k "${curl_args[@]}")
fi
header=$(curl "${curl_args[@]}" 2>/dev/null || true)
elif command -v wget &> /dev/null; then
local tmp
tmp=$(mktemp)
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
wget --spider --no-check-certificate --server-response "$url" >/dev/null 2>"$tmp" || true
else
wget --spider --server-response "$url" >/dev/null 2>"$tmp" || true
fi
header=$(cat "$tmp" 2>/dev/null || true)
rm -f "$tmp"
fi
if [[ -z "$header" ]]; then
return 1
fi
local checksum_line
checksum_line=$(printf '%s\n' "$header" | awk 'BEGIN{IGNORECASE=1} /^ *X-Checksum-Sha256:/{print $0; exit}')
if [[ -z "$checksum_line" ]]; then
return 1
fi
local value
value=$(printf '%s\n' "$checksum_line" | awk -F':' '{sub(/^[[:space:]]*/,"",$2); print $2}')
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
if [[ -z "$value" ]]; then
return 1
fi
FETCHED_CHECKSUM="$value"
return 0
}
calculate_sha256() {
local file="$1"
local hash=""
if command -v sha256sum &> /dev/null; then
hash=$(sha256sum "$file" | awk '{print $1}')
elif command -v shasum &> /dev/null; then
hash=$(shasum -a 256 "$file" | awk '{print $1}')
fi
if [[ -z "$hash" ]]; then
return 1
fi
CALCULATED_CHECKSUM=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]')
return 0
}
verify_agent_checksum() {
local url="$1"
if ! fetch_checksum_header "$url"; then
log_warn 'Agent download did not include X-Checksum-Sha256 header; skipping verification'
return 0
fi
if ! calculate_sha256 "$AGENT_PATH"; then
log_warn 'Unable to calculate sha256 checksum locally; skipping verification'
return 0
fi
if [[ "$FETCHED_CHECKSUM" != "$CALCULATED_CHECKSUM" ]]; then
rm -f "$AGENT_PATH"
log_error "Checksum mismatch. Expected $FETCHED_CHECKSUM but downloaded $CALCULATED_CHECKSUM"
return 1
fi
log_success 'Checksum verified for agent binary'
unset FETCHED_CHECKSUM CALCULATED_CHECKSUM
return 0
}
DOWNLOAD_SUCCESS_URL=""
if download_agent_from_url "$DOWNLOAD_URL"; then
:
DOWNLOAD_SUCCESS_URL="$DOWNLOAD_URL"
elif [[ "$DOWNLOAD_URL" != "$DOWNLOAD_URL_BASE" ]] && download_agent_from_url "$DOWNLOAD_URL_BASE"; then
log_info 'Falling back to server default agent binary'
DOWNLOAD_SUCCESS_URL="$DOWNLOAD_URL_BASE"
else
log_warn 'Failed to download agent binary'
log_warn "Ensure the Pulse server is reachable at $PRIMARY_URL"
exit 1
fi
if [[ -n "$DOWNLOAD_SUCCESS_URL" ]]; then
if ! verify_agent_checksum "$DOWNLOAD_SUCCESS_URL"; then
log_error 'Agent download failed checksum verification'
exit 1
fi
fi
chmod +x "$AGENT_PATH"
log_success "Agent binary installed"
@ -991,9 +1292,11 @@ allow_reenroll_if_needed() {
fi
if [[ "$success" == "true" ]]; then
log_success "Cleared any previous stop block for host"
log_success "Cleared previous removal block via Pulse API"
else
log_warn "Unable to confirm removal block clearance (continuing)"
log_warn 'Pulse still considers this host removed.'
log_warn 'If you just reinstalled, visit Pulse → Docker → Removed Hosts and allow re-enroll,'
log_warn 'or rerun this installer with an API token that includes the docker:manage scope.'
fi
return 0
@ -1070,31 +1373,49 @@ if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then
fi
fi
log_header 'Preparing service environment'
ensure_service_user
ensure_service_home
ensure_docker_group_membership
write_env_file
configure_polkit_rule
# Create systemd service
log_header 'Configuring systemd service'
SYSTEMD_ENV_TARGETS_LINE=""
if [[ -n "$JOINED_TARGETS" ]]; then
SYSTEMD_ENV_TARGETS_LINE="Environment=\"PULSE_TARGETS=$JOINED_TARGETS\""
fi
SYSTEMD_ENV_URL_LINE="Environment=\"PULSE_URL=$PRIMARY_URL\""
SYSTEMD_ENV_TOKEN_LINE="Environment=\"PULSE_TOKEN=$PRIMARY_TOKEN\""
SYSTEMD_ENV_INSECURE_LINE="Environment=\"PULSE_INSECURE_SKIP_VERIFY=$PRIMARY_INSECURE\""
cat > "$SERVICE_PATH" << EOF
[Unit]
Description=Pulse Docker Agent
After=network-online.target docker.service
Wants=network-online.target
After=network-online.target docker.socket docker.service
Wants=network-online.target docker.socket
[Service]
Type=simple
$SYSTEMD_ENV_URL_LINE
$SYSTEMD_ENV_TOKEN_LINE
$SYSTEMD_ENV_TARGETS_LINE
$SYSTEMD_ENV_INSECURE_LINE
EnvironmentFile=-$ENV_FILE
ExecStart=$AGENT_PATH --url "$PRIMARY_URL" --interval "$INTERVAL"$NO_AUTO_UPDATE_FLAG
Restart=on-failure
RestartSec=5s
User=root
StartLimitIntervalSec=120
StartLimitBurst=5
User=$SERVICE_USER_ACTUAL
Group=$SERVICE_GROUP_ACTUAL
$SYSTEMD_SUPPLEMENTARY_GROUPS_LINE
UMask=0077
NoNewPrivileges=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=read-only
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
ReadWritePaths=/var/run/docker.sock
ProtectHostname=yes
ProtectClock=yes
[Install]
WantedBy=multi-user.target

View file

@ -67,6 +67,10 @@ INTERVAL="30s"
UNINSTALL="false"
PLATFORM=""
FORCE=false
KEYCHAIN_ENABLED=true
KEYCHAIN_OPT_OUT=false
KEYCHAIN_OPT_OUT_REASON=""
USE_KEYCHAIN=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -94,6 +98,12 @@ while [[ $# -gt 0 ]]; do
FORCE=true
shift
;;
--no-keychain)
KEYCHAIN_ENABLED=false
KEYCHAIN_OPT_OUT=true
KEYCHAIN_OPT_OUT_REASON="flag"
shift
;;
*)
echo "Unknown option: $1"
exit 1
@ -136,6 +146,10 @@ fi
print_header
if [[ "$FORCE" == true ]]; then
log_warn "--force enabled: skipping interactive confirmations and accepting secure defaults."
fi
# Interactive prompts if parameters not provided (unless --force is used)
if [[ -z "$PULSE_URL" ]]; then
if [[ "$FORCE" == false ]]; then
@ -148,7 +162,10 @@ fi
if [[ -z "$PULSE_URL" ]]; then
log_error "Pulse URL is required"
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force]"
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force] [--no-keychain]"
echo ""
echo " --force Skip interactive prompts and accept secure defaults (including Keychain storage)."
echo " --no-keychain Disable Keychain storage and embed the token in the launch agent plist instead."
exit 1
fi
@ -416,8 +433,32 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
mkdir -p "$MACOS_LOG_DIR"
mkdir -p "$HOME/Library/LaunchAgents"
if [[ -n "$PULSE_TOKEN" && "$KEYCHAIN_ENABLED" == true && "$FORCE" == false ]]; then
echo ""
log_info "It is recommended to store the token in your Keychain so it never lands on disk."
KEYCHAIN_PROMPTED=false
if [[ -t 0 ]]; then
read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE
KEYCHAIN_PROMPTED=true
elif [[ -r /dev/tty ]]; then
read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE </dev/tty
KEYCHAIN_PROMPTED=true
else
log_warn "No interactive terminal detected; defaulting to Keychain storage. Use --no-keychain to opt out."
fi
if [[ "$KEYCHAIN_PROMPTED" == true && "$KEYCHAIN_RESPONSE" =~ ^[Nn] ]]; then
KEYCHAIN_ENABLED=false
KEYCHAIN_OPT_OUT=true
KEYCHAIN_OPT_OUT_REASON="prompt"
fi
echo ""
fi
# Store token in macOS Keychain for better security
if [[ -n "$PULSE_TOKEN" ]]; then
if [[ -n "$PULSE_TOKEN" && "$KEYCHAIN_ENABLED" == true ]]; then
log_info "For security, the token is stored in your macOS Keychain so it never lands on disk."
log_info "macOS may ask to allow access the first time the agent runs."
log_info "Use --no-keychain to opt out (the token will be embedded in the launchd plist instead)."
log_info "Storing token in macOS Keychain..."
# Delete existing keychain entry if it exists
@ -429,12 +470,13 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
KEYCHAIN_APPS=(
"/usr/local/bin/pulse-host-agent"
"/usr/local/bin/pulse-host-agent-wrapper.sh"
"/usr/bin/security"
)
KEYCHAIN_ARGS=()
for app in "${KEYCHAIN_APPS[@]}"; do
KEYCHAIN_ARGS+=(-T "$app")
if [[ -e "$app" ]]; then
KEYCHAIN_ARGS+=(-T "$app")
fi
done
if security add-generic-password \
@ -456,6 +498,17 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
log_info "You may need to grant Keychain access permissions"
USE_KEYCHAIN=false
fi
elif [[ -n "$PULSE_TOKEN" ]]; then
if [[ "$KEYCHAIN_OPT_OUT" == true ]]; then
if [[ "$KEYCHAIN_OPT_OUT_REASON" == "flag" ]]; then
log_warn "Keychain storage disabled via --no-keychain; token will be embedded in the launchd plist."
elif [[ "$KEYCHAIN_OPT_OUT_REASON" == "prompt" ]]; then
log_warn "Keychain storage skipped at user prompt; token will be embedded in the launchd plist."
fi
else
log_warn "Keychain storage disabled; token will be embedded in the launchd plist."
fi
USE_KEYCHAIN=false
else
USE_KEYCHAIN=false
fi

View file

@ -67,12 +67,19 @@ SOCKET_PATH="${RUNTIME_DIR}/pulse-sensor-proxy.sock"
WORK_DIR="/var/lib/pulse-sensor-proxy"
SSH_DIR="${WORK_DIR}/ssh"
CONFIG_DIR="/etc/pulse-sensor-proxy"
CTID_FILE="${CONFIG_DIR}/ctid"
CLEANUP_SCRIPT_PATH="/usr/local/bin/pulse-sensor-cleanup.sh"
CLEANUP_PATH_UNIT="/etc/systemd/system/pulse-sensor-cleanup.path"
CLEANUP_SERVICE_UNIT="/etc/systemd/system/pulse-sensor-cleanup.service"
CLEANUP_REQUEST_PATH="${WORK_DIR}/cleanup-request.json"
SERVICE_USER="pulse-sensor-proxy"
LOG_DIR="/var/log/pulse/sensor-proxy"
SHARE_DIR="/usr/local/share/pulse"
STORED_INSTALLER="${SHARE_DIR}/install-sensor-proxy.sh"
SELFHEAL_SCRIPT="/usr/local/bin/pulse-sensor-proxy-selfheal.sh"
SELFHEAL_SERVICE_UNIT="/etc/systemd/system/pulse-sensor-proxy-selfheal.service"
SELFHEAL_TIMER_UNIT="/etc/systemd/system/pulse-sensor-proxy-selfheal.timer"
SCRIPT_SOURCE="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || printf '%s' "${BASH_SOURCE[0]:-$0}")"
cleanup_local_authorized_keys() {
local auth_keys_file="/root/.ssh/authorized_keys"
@ -198,6 +205,34 @@ EOF
print_success "Removed cleanup service unit ${CLEANUP_SERVICE_UNIT}"
fi
if [[ -f "$SELFHEAL_TIMER_UNIT" ]]; then
systemctl stop pulse-sensor-proxy-selfheal.timer 2>/dev/null || true
systemctl disable pulse-sensor-proxy-selfheal.timer 2>/dev/null || true
rm -f "$SELFHEAL_TIMER_UNIT"
print_success "Removed self-heal timer ${SELFHEAL_TIMER_UNIT}"
fi
if [[ -f "$SELFHEAL_SERVICE_UNIT" ]]; then
systemctl stop pulse-sensor-proxy-selfheal.service 2>/dev/null || true
systemctl disable pulse-sensor-proxy-selfheal.service 2>/dev/null || true
rm -f "$SELFHEAL_SERVICE_UNIT"
print_success "Removed self-heal service ${SELFHEAL_SERVICE_UNIT}"
fi
if [[ -f "$SELFHEAL_SCRIPT" ]]; then
rm -f "$SELFHEAL_SCRIPT"
print_success "Removed self-heal helper ${SELFHEAL_SCRIPT}"
fi
if [[ -f "$STORED_INSTALLER" ]]; then
rm -f "$STORED_INSTALLER"
print_success "Removed cached installer ${STORED_INSTALLER}"
fi
if [[ -f "$CTID_FILE" ]]; then
rm -f "$CTID_FILE"
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload 2>/dev/null || true
fi
@ -558,6 +593,11 @@ install -d -o pulse-sensor-proxy -g pulse-sensor-proxy -m 0700 "$SSH_DIR"
install -m 0600 -o pulse-sensor-proxy -g pulse-sensor-proxy /dev/null "$SSH_DIR/known_hosts"
install -d -o pulse-sensor-proxy -g pulse-sensor-proxy -m 0755 /etc/pulse-sensor-proxy
if [[ -n "$CTID" ]]; then
echo "$CTID" > "$CTID_FILE"
chmod 0644 "$CTID_FILE"
fi
# Create config file with ACL for Docker containers (standalone mode)
if [[ "$STANDALONE" == true ]]; then
print_info "Creating config file with Docker container ACL..."
@ -946,7 +986,7 @@ LimitNOFILE=1024
SERVICE_EOF
# Enable and start the path unit
systemctl daemon-reload
systemctl daemon-reload || true
systemctl enable pulse-sensor-cleanup.path
systemctl start pulse-sensor-cleanup.path
@ -1261,7 +1301,7 @@ for key_type in id_rsa id_dsa id_ecdsa id_ed25519; do
print_warn "Found legacy SSH key: /root/.ssh/$key_type"
fi
pct exec "$CTID" -- rm -f "/root/.ssh/$key_type" "/root/.ssh/${key_type}.pub"
print_info " Removed /root/.ssh/$key_type"
print_info " Removed /root/.ssh/$key_type"
fi
done
@ -1272,6 +1312,88 @@ if [ "$LEGACY_KEYS_FOUND" = true ] && [ "$QUIET" != true ]; then
fi
fi # End of container-specific configuration
# Install self-heal safeguards to keep proxy available
print_info "Configuring self-heal safeguards..."
if [[ -n "$SCRIPT_SOURCE" && -f "$SCRIPT_SOURCE" ]]; then
install -d "$SHARE_DIR"
cp "$SCRIPT_SOURCE" "$STORED_INSTALLER"
chmod 0755 "$STORED_INSTALLER"
else
print_warn "Unable to cache installer script for self-heal (source path unavailable)"
fi
cat > "$SELFHEAL_SCRIPT" <<'EOF'
#!/bin/bash
set -euo pipefail
SERVICE="pulse-sensor-proxy"
INSTALLER="/usr/local/share/pulse/install-sensor-proxy.sh"
CTID_FILE="/etc/pulse-sensor-proxy/ctid"
LOG_TAG="pulse-sensor-proxy-selfheal"
log() {
logger -t "$LOG_TAG" "$1"
}
if ! command -v systemctl >/dev/null 2>&1; then
exit 0
fi
if ! systemctl list-unit-files | grep -q "^${SERVICE}\\.service"; then
if [[ -x "$INSTALLER" && -f "$CTID_FILE" ]]; then
log "Service unit missing; attempting reinstall"
bash "$INSTALLER" --ctid "$(cat "$CTID_FILE")" --skip-restart --quiet || log "Reinstall attempt failed"
fi
exit 0
fi
if ! systemctl is-active --quiet "${SERVICE}.service"; then
systemctl start "${SERVICE}.service" || true
sleep 2
fi
if ! systemctl is-active --quiet "${SERVICE}.service"; then
if [[ -x "$INSTALLER" && -f "$CTID_FILE" ]]; then
log "Service failed to start; attempting reinstall"
bash "$INSTALLER" --ctid "$(cat "$CTID_FILE")" --skip-restart --quiet || log "Reinstall attempt failed"
systemctl start "${SERVICE}.service" || true
fi
fi
EOF
chmod 0755 "$SELFHEAL_SCRIPT"
cat > "$SELFHEAL_SERVICE_UNIT" <<'EOF'
[Unit]
Description=Pulse Sensor Proxy Self-Heal
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pulse-sensor-proxy-selfheal.sh
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
cat > "$SELFHEAL_TIMER_UNIT" <<'EOF'
[Unit]
Description=Ensure pulse-sensor-proxy stays installed and running
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Unit=pulse-sensor-proxy-selfheal.service
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now pulse-sensor-proxy-selfheal.timer >/dev/null 2>&1 || true
if [ "$QUIET" = true ]; then
print_success "pulse-sensor-proxy installed and running"
else