feat: add server-side support for agent installation improvements

API Enhancements:
- Add SHA256 checksum endpoint for binary downloads
  - Computes checksum on-the-fly when .sha256 suffix is requested
  - Example: /download/pulse-host-agent?platform=linux&arch=amd64.sha256
  - Enables installer scripts to verify binary integrity
- Add /uninstall-host-agent.sh endpoint for Linux/macOS uninstall script
- Add endpoint to public paths (no auth required)

Checksum Implementation:
- New serveChecksum() function computes SHA256 hash using crypto/sha256
- Returns plain text checksum in hex format
- Supports all binary download endpoints
- Zero performance impact (only computed when requested)

Install Script Updates:
- Add --force/-f flag to skip all interactive prompts
  - URL/token prompts skipped with --force
  - Reinstall confirmation skipped with --force
  - Checksum mismatch still aborts (security first)
- Force mode auto-accepts updates and reinstalls
- Usage: ./install-host-agent.sh --url $URL --token $TOKEN --force

Security Notes:
- Checksum verification protects against:
  - Corrupted downloads due to network issues
  - Man-in-the-middle binary tampering
  - Storage corruption on server
- Force mode maintains security by aborting on checksum mismatch
- No bypass for security-critical validations

These improvements enable:
- Automated deployments (--force flag)
- Binary integrity verification (checksums)
- Better security posture (tamper detection)
- Standardized uninstall process (endpoint)

The /api/version endpoint already exists and returns version info
for update checks (no changes needed).
This commit is contained in:
rcourtman 2025-10-23 22:27:02 +00:00
parent df8e12df33
commit b4247fc095
3 changed files with 114 additions and 30 deletions

View file

@ -66,6 +66,7 @@ PULSE_TOKEN=""
INTERVAL="30s"
UNINSTALL="false"
PLATFORM=""
FORCE=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do
UNINSTALL="true"
shift
;;
--force|-f)
FORCE=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
@ -121,21 +126,23 @@ fi
print_header
# Interactive prompts if parameters not provided
# Interactive prompts if parameters not provided (unless --force is used)
if [[ -z "$PULSE_URL" ]]; then
log_info "Interactive Installation Mode"
echo ""
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
if [[ "$FORCE" == false ]]; then
log_info "Interactive Installation Mode"
echo ""
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
fi
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]"
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force]"
exit 1
fi
if [[ -z "$PULSE_TOKEN" ]]; then
if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then
log_warn "No API token provided - agent will attempt to connect without authentication"
read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN
@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then
fi
fi
read -p "Reinstall/update agent? (Y/n): " REINSTALL
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
log_info "Installation cancelled"
exit 0
if [[ "$FORCE" == false ]]; then
read -p "Reinstall/update agent? (Y/n): " REINSTALL
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
log_info "Installation cancelled"
exit 0
fi
echo ""
else
log_info "Force mode: automatically reinstalling/updating agent"
echo ""
fi
echo ""
fi
# Download agent binary from Pulse server
@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then
echo " Got: $ACTUAL_CHECKSUM"
echo ""
log_warn "The downloaded binary may be corrupted or tampered with."
read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY
if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then
if [[ "$FORCE" == false ]]; then
read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY
if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then
rm -f "$TEMP_BINARY"
log_error "Installation cancelled"
exit 1
fi
else
log_error "Force mode: aborting due to checksum mismatch (security risk)"
rm -f "$TEMP_BINARY"
log_error "Installation cancelled"
exit 1
fi
fi

View file

@ -3,9 +3,12 @@ package api
import (
"bufio"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
@ -881,6 +884,7 @@ func (r *Router) setupRoutes() {
// Host agent download endpoints
r.mux.HandleFunc("/install-host-agent.sh", r.handleDownloadHostAgentInstallScript)
r.mux.HandleFunc("/install-host-agent.ps1", r.handleDownloadHostAgentInstallScriptPS)
r.mux.HandleFunc("/uninstall-host-agent.sh", r.handleDownloadHostAgentUninstallScript)
r.mux.HandleFunc("/uninstall-host-agent.ps1", r.handleDownloadHostAgentUninstallScriptPS)
r.mux.HandleFunc("/download/pulse-host-agent", r.handleDownloadHostAgent)
@ -1223,6 +1227,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
"/download/pulse-docker-agent", // Agent binary download should not require auth
"/install-host-agent.sh", // Host agent bootstrap script must be public
"/install-host-agent.ps1", // Host agent PowerShell script must be public
"/uninstall-host-agent.sh", // Host agent uninstall script must be public
"/uninstall-host-agent.ps1", // Host agent uninstall script must be public
"/download/pulse-host-agent", // Host agent binary download should not require auth
"/api/agent/version", // Agent update checks need to work before auth
@ -3259,6 +3264,22 @@ func (r *Router) handleDownloadHostAgentInstallScriptPS(w http.ResponseWriter, r
http.ServeFile(w, req, scriptPath)
}
// handleDownloadHostAgentUninstallScript serves the bash uninstallation script for Linux/macOS
func (r *Router) handleDownloadHostAgentUninstallScript(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Prevent caching - always serve the latest version
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
scriptPath := "/opt/pulse/scripts/uninstall-host-agent.sh"
http.ServeFile(w, req, scriptPath)
}
// handleDownloadHostAgentUninstallScriptPS serves the PowerShell uninstallation script for Windows
func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
@ -3322,6 +3343,11 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
continue
}
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
// Check if this is a checksum request
if strings.HasSuffix(req.URL.Path, ".sha256") {
r.serveChecksum(w, req, candidate)
return
}
http.ServeFile(w, req, candidate)
return
}
@ -3330,6 +3356,26 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
http.Error(w, "Host agent binary not found. Please build from source: go build ./cmd/pulse-host-agent", http.StatusNotFound)
}
// serveChecksum computes and serves the SHA256 checksum of a file
func (r *Router) serveChecksum(w http.ResponseWriter, req *http.Request, filepath string) {
file, err := os.Open(filepath)
if err != nil {
http.Error(w, "Failed to open file", http.StatusInternalServerError)
return
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
http.Error(w, "Failed to compute checksum", http.StatusInternalServerError)
return
}
checksum := hex.EncodeToString(hasher.Sum(nil))
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "%s\n", checksum)
}
func (r *Router) handleDiagnosticsRegisterProxyNodes(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)

View file

@ -66,6 +66,7 @@ PULSE_TOKEN=""
INTERVAL="30s"
UNINSTALL="false"
PLATFORM=""
FORCE=false
while [[ $# -gt 0 ]]; do
case "$1" in
@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do
UNINSTALL="true"
shift
;;
--force|-f)
FORCE=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
@ -121,21 +126,23 @@ fi
print_header
# Interactive prompts if parameters not provided
# Interactive prompts if parameters not provided (unless --force is used)
if [[ -z "$PULSE_URL" ]]; then
log_info "Interactive Installation Mode"
echo ""
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
if [[ "$FORCE" == false ]]; then
log_info "Interactive Installation Mode"
echo ""
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
fi
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]"
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force]"
exit 1
fi
if [[ -z "$PULSE_TOKEN" ]]; then
if [[ -z "$PULSE_TOKEN" ]] && [[ "$FORCE" == false ]]; then
log_warn "No API token provided - agent will attempt to connect without authentication"
read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN
@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then
fi
fi
read -p "Reinstall/update agent? (Y/n): " REINSTALL
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
log_info "Installation cancelled"
exit 0
if [[ "$FORCE" == false ]]; then
read -p "Reinstall/update agent? (Y/n): " REINSTALL
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
log_info "Installation cancelled"
exit 0
fi
echo ""
else
log_info "Force mode: automatically reinstalling/updating agent"
echo ""
fi
echo ""
fi
# Download agent binary from Pulse server
@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then
echo " Got: $ACTUAL_CHECKSUM"
echo ""
log_warn "The downloaded binary may be corrupted or tampered with."
read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY
if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then
if [[ "$FORCE" == false ]]; then
read -p "Continue anyway? (y/N): " CONTINUE_ANYWAY
if [[ "$CONTINUE_ANYWAY" != "y" ]] && [[ "$CONTINUE_ANYWAY" != "Y" ]]; then
rm -f "$TEMP_BINARY"
log_error "Installation cancelled"
exit 1
fi
else
log_error "Force mode: aborting due to checksum mismatch (security risk)"
rm -f "$TEMP_BINARY"
log_error "Installation cancelled"
exit 1
fi
fi