mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
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:
parent
df8e12df33
commit
b4247fc095
3 changed files with 114 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue