From 07d838185822910c0e6ccd183fed9166a4eebfce Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Mon, 24 Nov 2025 14:28:09 +0000 Subject: [PATCH] Fix host agent registration verification issues (#746) - Change default server listen addresses to empty string (listen on all interfaces including IPv6) - Add short hostname matching fallback in host lookup API to handle FQDN vs short name mismatches - Implement retry loop (30s) in both Windows and Linux/macOS installers for registration verification - Fix lint errors: remove unnecessary fmt.Sprintf and nil checks before len() This resolves the 'Installer could not yet confirm host registration with Pulse' warning by addressing timing issues, hostname matching, and network connectivity. --- frontend-modern/public/install-host-agent.ps1 | 21 +++++++++--- internal/api/auth.go | 2 +- internal/api/docker_agents.go | 2 +- internal/api/host_agents.go | 21 ++++++++++++ internal/api/notifications.go | 4 +-- internal/config/config.go | 8 ++--- scripts/install-host-agent.sh | 33 ++++++++++++++----- 7 files changed, 71 insertions(+), 20 deletions(-) diff --git a/frontend-modern/public/install-host-agent.ps1 b/frontend-modern/public/install-host-agent.ps1 index ad1e107..5a11761 100644 --- a/frontend-modern/public/install-host-agent.ps1 +++ b/frontend-modern/public/install-host-agent.ps1 @@ -455,11 +455,24 @@ if (-not $NoService) { if ($status -eq 'Running') { PulseSuccess "Service started successfully!" - PulseInfo "Waiting 10 seconds to validate agent reporting..." - Start-Sleep -Seconds 10 + PulseInfo "Waiting for agent to register with Pulse (up to 30s)..." + + $maxRetries = 15 + $retryDelay = 2 + $lookupHost = $null + + for ($i = 1; $i -le $maxRetries; $i++) { + Start-Sleep -Seconds $retryDelay + $hostname = $env:COMPUTERNAME + $lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token + + if ($lookupHost) { + break + } + Write-Host "." -NoNewline + } + Write-Host "" # Newline after dots - $hostname = $env:COMPUTERNAME - $lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token if ($lookupHost) { PulseSuccess "Agent successfully registered with Pulse (host '$hostname')." if ($lookupHost.status) { diff --git a/internal/api/auth.go b/internal/api/auth.go index cc71469..f83b261 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -482,7 +482,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool w.Write([]byte(fmt.Sprintf(`{"error":"Invalid credentials","attempts":%d,"remaining":%d,"maxAttempts":%d}`, attempts, remaining, maxFailedAttempts))) } else { - w.Write([]byte(fmt.Sprintf(`{"error":"Invalid credentials","locked":true,"message":"Account locked for 15 minutes"}`))) + w.Write([]byte(`{"error":"Invalid credentials","locked":true,"message":"Account locked for 15 minutes"}`)) } return false } diff --git a/internal/api/docker_agents.go b/internal/api/docker_agents.go index 43db071..f5cdfe3 100644 --- a/internal/api/docker_agents.go +++ b/internal/api/docker_agents.go @@ -82,7 +82,7 @@ func (h *DockerAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Reques "id": cmd.ID, "type": cmd.Type, } - if payload != nil && len(payload) > 0 { + if len(payload) > 0 { commandResponse["payload"] = payload } response["commands"] = []map[string]any{commandResponse} diff --git a/internal/api/host_agents.go b/internal/api/host_agents.go index 97581cd..7931ded 100644 --- a/internal/api/host_agents.go +++ b/internal/api/host_agents.go @@ -112,6 +112,7 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request) } if !found && hostname != "" { + // First pass: exact match (case-insensitive) for _, candidate := range state.Hosts { if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) { host = candidate @@ -119,6 +120,26 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request) break } } + + // Second pass: short hostname match (if exact match failed) + if !found { + // Helper to get short hostname (before first dot) + getShortName := func(h string) string { + if idx := strings.Index(h, "."); idx != -1 { + return h[:idx] + } + return h + } + + shortLookup := getShortName(hostname) + for _, candidate := range state.Hosts { + if strings.EqualFold(getShortName(candidate.Hostname), shortLookup) { + host = candidate + found = true + break + } + } + } } if !found { diff --git a/internal/api/notifications.go b/internal/api/notifications.go index cb51924..50a08c0 100644 --- a/internal/api/notifications.go +++ b/internal/api/notifications.go @@ -156,7 +156,7 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques } // Mask headers - only show keys, not values - if webhook.Headers != nil && len(webhook.Headers) > 0 { + if len(webhook.Headers) > 0 { maskedHeaders := make(map[string]string) for key := range webhook.Headers { maskedHeaders[key] = "***REDACTED***" @@ -165,7 +165,7 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques } // Mask custom fields - only show keys, not values - if webhook.CustomFields != nil && len(webhook.CustomFields) > 0 { + if len(webhook.CustomFields) > 0 { maskedFields := make(map[string]string) for key := range webhook.CustomFields { maskedFields[key] = "***REDACTED***" diff --git a/internal/config/config.go b/internal/config/config.go index d1869d0..f9d41ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -71,9 +71,9 @@ func IsPasswordHashed(password string) bool { // NOTE: The envconfig tags are legacy and not used - configuration is loaded from encrypted JSON files type Config struct { // Server settings - BackendHost string `envconfig:"BACKEND_HOST" default:"0.0.0.0"` + BackendHost string `envconfig:"BACKEND_HOST" default:""` BackendPort int `envconfig:"BACKEND_PORT" default:"3000"` - FrontendHost string `envconfig:"FRONTEND_HOST" default:"0.0.0.0"` + FrontendHost string `envconfig:"FRONTEND_HOST" default:""` FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"` ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"` DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"` @@ -523,9 +523,9 @@ func Load() (*Config, error) { // Initialize config with defaults cfg := &Config{ - BackendHost: "0.0.0.0", + BackendHost: "", BackendPort: 3000, - FrontendHost: "0.0.0.0", + FrontendHost: "", FrontendPort: 7655, ConfigPath: dataDir, DataPath: dataDir, diff --git a/scripts/install-host-agent.sh b/scripts/install-host-agent.sh index 7a401b6..f0999cb 100755 --- a/scripts/install-host-agent.sh +++ b/scripts/install-host-agent.sh @@ -1087,14 +1087,31 @@ if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then if [[ -z "$PULSE_TOKEN" ]]; then log_info "Registration check skipped (no API token available for lookup)." elif command -v curl &> /dev/null; then - log_info "Verifying agent registration with Pulse server..." - LOOKUP_RESPONSE=$(curl -fsSL \ - -H "Authorization: Bearer $PULSE_TOKEN" \ - --get \ - --data-urlencode "hostname=$HOSTNAME" \ - "$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true) + log_info "Verifying agent registration with Pulse server (up to 30s)..." + + MAX_RETRIES=15 + RETRY_DELAY=2 + LOOKUP_SUCCESS=false + + for ((i=1; i<=MAX_RETRIES; i++)); do + LOOKUP_RESPONSE=$(curl -fsSL \ + -H "Authorization: Bearer $PULSE_TOKEN" \ + --get \ + --data-urlencode "hostname=$HOSTNAME" \ + "$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true) - if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then + if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then + LOOKUP_SUCCESS=true + break + fi + + # Print a dot without newline to show progress + printf "." + sleep $RETRY_DELAY + done + echo "" # Newline after dots + + if [[ "$LOOKUP_SUCCESS" == true ]]; then host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p') log_success "Agent successfully registered with Pulse server!" @@ -1102,7 +1119,7 @@ if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then log_info "Pulse reports status: $host_status (last seen $last_seen)" fi else - log_warn "Agent lookup did not confirm registration yet (response: ${LOOKUP_RESPONSE:-no data})." + log_warn "Agent lookup did not confirm registration after 30s (response: ${LOOKUP_RESPONSE:-no data})." log_info "Service is running; metrics should appear shortly." fi else