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"
|
INTERVAL="30s"
|
||||||
UNINSTALL="false"
|
UNINSTALL="false"
|
||||||
PLATFORM=""
|
PLATFORM=""
|
||||||
|
FORCE=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do
|
||||||
UNINSTALL="true"
|
UNINSTALL="true"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--force|-f)
|
||||||
|
FORCE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1"
|
echo "Unknown option: $1"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -121,21 +126,23 @@ fi
|
||||||
|
|
||||||
print_header
|
print_header
|
||||||
|
|
||||||
# Interactive prompts if parameters not provided
|
# Interactive prompts if parameters not provided (unless --force is used)
|
||||||
if [[ -z "$PULSE_URL" ]]; then
|
if [[ -z "$PULSE_URL" ]]; then
|
||||||
log_info "Interactive Installation Mode"
|
if [[ "$FORCE" == false ]]; then
|
||||||
echo ""
|
log_info "Interactive Installation Mode"
|
||||||
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
|
echo ""
|
||||||
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
|
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
|
fi
|
||||||
|
|
||||||
if [[ -z "$PULSE_URL" ]]; then
|
if [[ -z "$PULSE_URL" ]]; then
|
||||||
log_error "Pulse URL is required"
|
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
|
exit 1
|
||||||
fi
|
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"
|
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
|
read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN
|
||||||
|
|
||||||
|
|
@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -p "Reinstall/update agent? (Y/n): " REINSTALL
|
if [[ "$FORCE" == false ]]; then
|
||||||
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
|
read -p "Reinstall/update agent? (Y/n): " REINSTALL
|
||||||
log_info "Installation cancelled"
|
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
|
||||||
exit 0
|
log_info "Installation cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
log_info "Force mode: automatically reinstalling/updating agent"
|
||||||
|
echo ""
|
||||||
fi
|
fi
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download agent binary from Pulse server
|
# Download agent binary from Pulse server
|
||||||
|
|
@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then
|
||||||
echo " Got: $ACTUAL_CHECKSUM"
|
echo " Got: $ACTUAL_CHECKSUM"
|
||||||
echo ""
|
echo ""
|
||||||
log_warn "The downloaded binary may be corrupted or tampered with."
|
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"
|
rm -f "$TEMP_BINARY"
|
||||||
log_error "Installation cancelled"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ package api
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -881,6 +884,7 @@ func (r *Router) setupRoutes() {
|
||||||
// Host agent download endpoints
|
// Host agent download endpoints
|
||||||
r.mux.HandleFunc("/install-host-agent.sh", r.handleDownloadHostAgentInstallScript)
|
r.mux.HandleFunc("/install-host-agent.sh", r.handleDownloadHostAgentInstallScript)
|
||||||
r.mux.HandleFunc("/install-host-agent.ps1", r.handleDownloadHostAgentInstallScriptPS)
|
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("/uninstall-host-agent.ps1", r.handleDownloadHostAgentUninstallScriptPS)
|
||||||
r.mux.HandleFunc("/download/pulse-host-agent", r.handleDownloadHostAgent)
|
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
|
"/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.sh", // Host agent bootstrap script must be public
|
||||||
"/install-host-agent.ps1", // Host agent PowerShell 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
|
"/uninstall-host-agent.ps1", // Host agent uninstall script must be public
|
||||||
"/download/pulse-host-agent", // Host agent binary download should not require auth
|
"/download/pulse-host-agent", // Host agent binary download should not require auth
|
||||||
"/api/agent/version", // Agent update checks need to work before 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)
|
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
|
// handleDownloadHostAgentUninstallScriptPS serves the PowerShell uninstallation script for Windows
|
||||||
func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, req *http.Request) {
|
func (r *Router) handleDownloadHostAgentUninstallScriptPS(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
|
|
@ -3322,6 +3343,11 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
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)
|
http.ServeFile(w, req, candidate)
|
||||||
return
|
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)
|
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) {
|
func (r *Router) handleDiagnosticsRegisterProxyNodes(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed", nil)
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ PULSE_TOKEN=""
|
||||||
INTERVAL="30s"
|
INTERVAL="30s"
|
||||||
UNINSTALL="false"
|
UNINSTALL="false"
|
||||||
PLATFORM=""
|
PLATFORM=""
|
||||||
|
FORCE=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do
|
||||||
UNINSTALL="true"
|
UNINSTALL="true"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--force|-f)
|
||||||
|
FORCE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $1"
|
echo "Unknown option: $1"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -121,21 +126,23 @@ fi
|
||||||
|
|
||||||
print_header
|
print_header
|
||||||
|
|
||||||
# Interactive prompts if parameters not provided
|
# Interactive prompts if parameters not provided (unless --force is used)
|
||||||
if [[ -z "$PULSE_URL" ]]; then
|
if [[ -z "$PULSE_URL" ]]; then
|
||||||
log_info "Interactive Installation Mode"
|
if [[ "$FORCE" == false ]]; then
|
||||||
echo ""
|
log_info "Interactive Installation Mode"
|
||||||
read -p "Enter Pulse server URL (e.g., http://pulse.example.com:7656): " PULSE_URL
|
echo ""
|
||||||
PULSE_URL=$(echo "$PULSE_URL" | sed 's:/*$::') # Remove trailing slashes
|
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
|
fi
|
||||||
|
|
||||||
if [[ -z "$PULSE_URL" ]]; then
|
if [[ -z "$PULSE_URL" ]]; then
|
||||||
log_error "Pulse URL is required"
|
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
|
exit 1
|
||||||
fi
|
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"
|
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
|
read -p "Enter API token (or press Enter to skip): " PULSE_TOKEN
|
||||||
|
|
||||||
|
|
@ -224,12 +231,17 @@ if [[ -f "$AGENT_PATH" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -p "Reinstall/update agent? (Y/n): " REINSTALL
|
if [[ "$FORCE" == false ]]; then
|
||||||
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
|
read -p "Reinstall/update agent? (Y/n): " REINSTALL
|
||||||
log_info "Installation cancelled"
|
if [[ "$REINSTALL" == "n" ]] || [[ "$REINSTALL" == "N" ]]; then
|
||||||
exit 0
|
log_info "Installation cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
log_info "Force mode: automatically reinstalling/updating agent"
|
||||||
|
echo ""
|
||||||
fi
|
fi
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Download agent binary from Pulse server
|
# Download agent binary from Pulse server
|
||||||
|
|
@ -322,10 +334,17 @@ if [[ -n "$EXPECTED_CHECKSUM" ]]; then
|
||||||
echo " Got: $ACTUAL_CHECKSUM"
|
echo " Got: $ACTUAL_CHECKSUM"
|
||||||
echo ""
|
echo ""
|
||||||
log_warn "The downloaded binary may be corrupted or tampered with."
|
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"
|
rm -f "$TEMP_BINARY"
|
||||||
log_error "Installation cancelled"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue