mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Fix PVE token re-registration after agent reinstall
When the agent is reinstalled on a Proxmox host, it rotates the PVE API token in Proxmox but the Pulse server's /api/setup-script-url endpoint requires settings:write scope — agent tokens only have agent:report — so the 401 aborted the update, leaving Pulse with a stale token and a disconnected PVE node. Three-part fix: - server: accept agent API tokens on /api/auto-register for updating existing nodes (new nodes still require setup-token auth) - agent: fall through instead of aborting when setup token fetch returns 4xx; send X-API-Token header so the server can authenticate via the agent token instead - update: allow HTTP auto-update URLs for RFC 1918 private network addresses (LAN installs without HTTPS no longer block auto-update)
This commit is contained in:
parent
f754dfa9b9
commit
b0b790cf55
4 changed files with 94 additions and 11 deletions
|
|
@ -568,6 +568,14 @@ func isLoopbackHost(host string) bool {
|
|||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func isPrivateNetworkHost(host string) bool {
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(strings.Trim(host, "[]"))
|
||||
return ip != nil && ip.IsPrivate()
|
||||
}
|
||||
|
||||
func (u *Updater) validatePulseURL() error {
|
||||
pulseURL := strings.TrimSpace(u.cfg.PulseURL)
|
||||
if pulseURL == "" {
|
||||
|
|
@ -593,10 +601,10 @@ func (u *Updater) validatePulseURL() error {
|
|||
case "https":
|
||||
return nil
|
||||
case "http":
|
||||
if u.cfg.InsecureSkipVerify || isLoopbackHost(parsed.Hostname()) {
|
||||
if u.cfg.InsecureSkipVerify || isLoopbackHost(parsed.Hostname()) || isPrivateNetworkHost(parsed.Hostname()) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("HTTP Pulse URL is only allowed for localhost/loopback or when insecure mode is enabled")
|
||||
return fmt.Errorf("HTTP Pulse URL is only allowed for localhost/loopback, private networks, or when insecure mode is enabled")
|
||||
default:
|
||||
return fmt.Errorf("unsupported Pulse URL scheme %q", parsed.Scheme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// agentAutoRegContextKey marks a request authenticated via agent:report token.
|
||||
// When set, handleCanonicalAutoRegister restricts to updating existing nodes only.
|
||||
type agentAutoRegContextKey struct{}
|
||||
|
||||
func isAgentAutoRegAuth(ctx context.Context) bool {
|
||||
v, _ := ctx.Value(agentAutoRegContextKey{}).(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// HandleSetupScript serves the setup script for Proxmox/PBS nodes
|
||||
func (h *ConfigHandlers) handleSetupScript(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
|
|
@ -646,6 +655,32 @@ func (h *ConfigHandlers) handleAutoRegister(w http.ResponseWriter, r *http.Reque
|
|||
h.codeMutex.Unlock()
|
||||
}
|
||||
|
||||
// Fallback: allow agents with a valid agent:report API token to re-register their
|
||||
// Proxmox node. This is needed when an agent reinstalls — it rotates the PVE token
|
||||
// but can't fetch a one-time setup token (that endpoint requires settings:write).
|
||||
// For security this path is restricted to updating existing nodes only.
|
||||
if !authenticated {
|
||||
if apiToken, hasToken := explicitAPITokenFromRequest(r); hasToken {
|
||||
config.Mu.RLock()
|
||||
cfg := h.getConfig(r.Context())
|
||||
isValid := cfg != nil && cfg.IsValidAPIToken(apiToken)
|
||||
config.Mu.RUnlock()
|
||||
if isValid {
|
||||
config.Mu.Lock()
|
||||
record, ok := cfg.ValidateAPIToken(apiToken)
|
||||
config.Mu.Unlock()
|
||||
if ok && record != nil && record.HasScope(config.ScopeAgentReport) {
|
||||
authenticated = true
|
||||
r = r.WithContext(context.WithValue(r.Context(), agentAutoRegContextKey{}, true))
|
||||
log.Info().
|
||||
Str("type", req.Type).
|
||||
Str("host", req.Host).
|
||||
Msg("Auto-register authenticated via agent API token (update-only mode)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Abort when no authentication succeeded. This applies even when API tokens
|
||||
// are not configured to ensure one-time setup tokens are always required.
|
||||
if !authenticated {
|
||||
|
|
@ -859,6 +894,12 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h
|
|||
instance.VerifySSL = pveNode.VerifySSL
|
||||
log.Info().Str("host", host).Str("type", "pve").Msg(canonicalAutoRegisterMatchMessage("host; updated token in-place"))
|
||||
} else {
|
||||
// Agent-token auth is restricted to updating existing nodes only.
|
||||
if isAgentAutoRegAuth(r.Context()) {
|
||||
log.Warn().Str("host", host).Str("type", "pve").Msg("Agent API token auth rejected for new PVE node registration")
|
||||
http.Error(w, "agent token auth permits token updates for existing nodes only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
|
||||
Source: unifiedresources.SourceProxmox,
|
||||
Type: unifiedresources.ResourceTypeAgent,
|
||||
|
|
@ -934,6 +975,12 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h
|
|||
instance.VerifySSL = pbsNode.VerifySSL
|
||||
log.Info().Str("host", host).Str("type", "pbs").Msg(canonicalAutoRegisterMatchMessage("host; updated token in-place"))
|
||||
} else {
|
||||
// Agent-token auth is restricted to updating existing nodes only.
|
||||
if isAgentAutoRegAuth(r.Context()) {
|
||||
log.Warn().Str("host", host).Str("type", "pbs").Msg("Agent API token auth rejected for new PBS node registration")
|
||||
http.Error(w, "agent token auth permits token updates for existing nodes only", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
|
||||
Source: unifiedresources.SourcePBS,
|
||||
Type: unifiedresources.ResourceTypePBS,
|
||||
|
|
|
|||
|
|
@ -4869,8 +4869,9 @@ func (r *Router) handleAgentVersion(w http.ResponseWriter, req *http.Request) {
|
|||
// Return the server version - all agents should match the server version.
|
||||
// Dev/git builds return "dev" so agents don't enter an infinite update loop
|
||||
// (the agent skips updates when the server reports "dev").
|
||||
versionInfo, err := updates.GetCurrentVersion()
|
||||
version := "dev"
|
||||
if versionInfo, err := updates.GetCurrentVersion(); err == nil && !versionInfo.IsDevelopment {
|
||||
if err == nil && !versionInfo.IsDevelopment {
|
||||
version = versionInfo.Version
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -547,7 +547,17 @@ func (p *ProxmoxSetup) orderedCandidateHosts(ctx context.Context, ptype proxmoxP
|
|||
func (p *ProxmoxSetup) checkRegistrationWithPulse(ctx context.Context, ptype proxmoxProductType, hostURL string) (bool, error) {
|
||||
setupToken, err := p.fetchSetupToken(ctx, ptype, hostURL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("fetch setup token: %w", err)
|
||||
var ce *clientError
|
||||
var pe *permanentError
|
||||
if errors.As(err, &pe) {
|
||||
return false, fmt.Errorf("fetch setup token: %w", err)
|
||||
}
|
||||
if errors.As(err, &ce) {
|
||||
// Fall through without a setup token; server will authenticate via X-API-Token.
|
||||
setupToken = ""
|
||||
} else {
|
||||
return false, fmt.Errorf("fetch setup token: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
payload := autoRegisterRequest{
|
||||
|
|
@ -570,6 +580,9 @@ func (p *ProxmoxSetup) checkRegistrationWithPulse(ctx context.Context, ptype pro
|
|||
return false, fmt.Errorf("create registration-check request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token := strings.TrimSpace(p.apiToken); token != "" {
|
||||
req.Header.Set("X-API-Token", token)
|
||||
}
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -1143,6 +1156,9 @@ func (p *ProxmoxSetup) doRegisterRequest(ctx context.Context, body []byte, expec
|
|||
return autoRegisterResponse{}, &permanentError{fmt.Errorf("create request: %w", err)}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token := strings.TrimSpace(p.apiToken); token != "" {
|
||||
req.Header.Set("X-API-Token", token)
|
||||
}
|
||||
|
||||
resp, err := p.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -1327,15 +1343,26 @@ func (p *ProxmoxSetup) registerWithPulse(ctx context.Context, ptype proxmoxProdu
|
|||
lastErr = fmt.Errorf("fetch setup token: %w", err)
|
||||
var ce *clientError
|
||||
var pe *permanentError
|
||||
if errors.As(lastErr, &ce) || errors.As(lastErr, &pe) {
|
||||
if errors.As(lastErr, &pe) {
|
||||
return autoRegisterResponse{}, lastErr
|
||||
}
|
||||
p.logger.Warn().Err(lastErr).
|
||||
Int("attempt", attempt+1).
|
||||
Int("max_attempts", len(backoffs)+1).
|
||||
Str("type", string(ptype)).
|
||||
Msg("Proxmox setup token fetch failed")
|
||||
continue
|
||||
if errors.As(lastErr, &ce) {
|
||||
// The setup-script-url endpoint requires settings:write scope; agent tokens
|
||||
// only have agent:report. Fall through without a setup token — the server
|
||||
// will authenticate via the X-API-Token header instead (update-only path).
|
||||
p.logger.Info().
|
||||
Int("statusCode", ce.statusCode).
|
||||
Str("type", string(ptype)).
|
||||
Msg("Setup token fetch returned client error; falling back to agent API token auth")
|
||||
setupToken = ""
|
||||
} else {
|
||||
p.logger.Warn().Err(lastErr).
|
||||
Int("attempt", attempt+1).
|
||||
Int("max_attempts", len(backoffs)+1).
|
||||
Str("type", string(ptype)).
|
||||
Msg("Proxmox setup token fetch failed")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
payload := autoRegisterRequest{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue