mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-11 08:51:17 +00:00
Reject shared Docker token host identity collisions (#1366)
This commit is contained in:
parent
6c03706b6f
commit
333e66a8e9
4 changed files with 130 additions and 0 deletions
|
|
@ -87,6 +87,9 @@ func findMatchingDockerHost(hosts []models.DockerHost, report agentsdocker.Repor
|
|||
|
||||
existingToken := strings.TrimSpace(host.TokenID)
|
||||
if tokenID == "" || existingToken == tokenID {
|
||||
if dockerHostIdentityConflicts(host, report) {
|
||||
continue
|
||||
}
|
||||
return host, true
|
||||
}
|
||||
}
|
||||
|
|
@ -132,6 +135,41 @@ func findMatchingDockerHost(hosts []models.DockerHost, report agentsdocker.Repor
|
|||
return models.DockerHost{}, false
|
||||
}
|
||||
|
||||
// dockerHostIdentityConflicts reports whether an incoming report is clearly from
|
||||
// a different physical/swarm node than the existing Docker host record.
|
||||
func dockerHostIdentityConflicts(existing models.DockerHost, report agentsdocker.Report) bool {
|
||||
existingMachineID := strings.TrimSpace(existing.MachineID)
|
||||
reportMachineID := strings.TrimSpace(report.Host.MachineID)
|
||||
if existingMachineID != "" && reportMachineID != "" && existingMachineID != reportMachineID {
|
||||
return true
|
||||
}
|
||||
|
||||
existingSwarmNodeID := ""
|
||||
if existing.Swarm != nil {
|
||||
existingSwarmNodeID = strings.TrimSpace(existing.Swarm.NodeID)
|
||||
}
|
||||
reportSwarmNodeID := ""
|
||||
if report.Host.Swarm != nil {
|
||||
reportSwarmNodeID = strings.TrimSpace(report.Host.Swarm.NodeID)
|
||||
}
|
||||
if existingSwarmNodeID != "" && reportSwarmNodeID != "" && existingSwarmNodeID != reportSwarmNodeID {
|
||||
return true
|
||||
}
|
||||
|
||||
// When we do not have stronger stable IDs, a hostname change is ambiguous
|
||||
// and should not be treated as the same reporting host automatically.
|
||||
existingHostname := strings.TrimSpace(existing.Hostname)
|
||||
reportHostname := strings.TrimSpace(report.Host.Hostname)
|
||||
if existingMachineID == "" && reportMachineID == "" &&
|
||||
existingSwarmNodeID == "" && reportSwarmNodeID == "" &&
|
||||
existingHostname != "" && reportHostname != "" &&
|
||||
!strings.EqualFold(existingHostname, reportHostname) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// dockerHostIDExists checks if a host ID is already in use.
|
||||
func dockerHostIDExists(id string, hosts []models.DockerHost) bool {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
|
|
|
|||
|
|
@ -804,6 +804,22 @@ func TestResolveDockerHostIdentifier(t *testing.T) {
|
|||
expectedID: "existing-host",
|
||||
expectFallbacks: 1,
|
||||
},
|
||||
{
|
||||
name: "agent id match is ignored when host identity conflicts",
|
||||
report: agentsdocker.Report{
|
||||
Agent: agentsdocker.AgentInfo{ID: "agent-1"},
|
||||
Host: agentsdocker.HostInfo{
|
||||
Hostname: "node-b",
|
||||
},
|
||||
},
|
||||
tokenRecord: &config.APITokenRecord{ID: "token-1"},
|
||||
hosts: []models.DockerHost{
|
||||
{ID: "existing-host", AgentID: "agent-1", TokenID: "token-1", Hostname: "node-a"},
|
||||
},
|
||||
expectMatch: false,
|
||||
expectedID: "agent-1",
|
||||
expectFallbacks: 2,
|
||||
},
|
||||
{
|
||||
name: "no match, base available",
|
||||
report: agentsdocker.Report{
|
||||
|
|
|
|||
|
|
@ -2520,6 +2520,34 @@ func (m *Monitor) ApplyDockerReport(report agentsdocker.Report, tokenRecord *con
|
|||
Msg("Rejecting Docker report: token already bound to different agent")
|
||||
return models.DockerHost{}, fmt.Errorf("API token%s is already in use by agent %q (host: %s). Each Docker agent must use a unique API token. Generate a new token for this agent", tokenHint, boundAgentID, conflictingHostname)
|
||||
}
|
||||
|
||||
for _, host := range hostsSnapshot {
|
||||
if strings.TrimSpace(host.TokenID) != tokenID || strings.TrimSpace(host.AgentID) != agentID {
|
||||
continue
|
||||
}
|
||||
if !dockerHostIdentityConflicts(host, report) {
|
||||
continue
|
||||
}
|
||||
m.mu.Unlock()
|
||||
conflictingHost := host.Hostname
|
||||
if host.CustomDisplayName != "" {
|
||||
conflictingHost = host.CustomDisplayName
|
||||
} else if host.DisplayName != "" {
|
||||
conflictingHost = host.DisplayName
|
||||
}
|
||||
tokenHint := tokenHintFromRecord(tokenRecord)
|
||||
if tokenHint != "" {
|
||||
tokenHint = " (" + tokenHint + ")"
|
||||
}
|
||||
log.Warn().
|
||||
Str("tokenID", tokenID).
|
||||
Str("agentID", agentID).
|
||||
Str("existingHostID", host.ID).
|
||||
Str("existingHostname", host.Hostname).
|
||||
Str("reportingHostname", report.Host.Hostname).
|
||||
Msg("Rejecting Docker report: shared token/agent identity conflicts with existing host")
|
||||
return models.DockerHost{}, fmt.Errorf("API token%s is already bound to Docker host %q. Reusing the same token on another node is not supported. Generate a dedicated token for this Docker agent", tokenHint, conflictingHost)
|
||||
}
|
||||
} else {
|
||||
// First time seeing this token - bind it to this agent
|
||||
m.dockerTokenBindings[tokenID] = agentID
|
||||
|
|
|
|||
|
|
@ -848,6 +848,54 @@ func TestApplyDockerReport_TokenBoundToDifferentAgent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestApplyDockerReport_SameTokenAndAgentIdentityDifferentHostnameRejected(t *testing.T) {
|
||||
monitor := newTestMonitor(t)
|
||||
|
||||
token := &config.APITokenRecord{ID: "shared-token", Name: "Shared Token"}
|
||||
baseTime := time.Now().UTC()
|
||||
|
||||
firstReport := agentsdocker.Report{
|
||||
Agent: agentsdocker.AgentInfo{
|
||||
ID: "shared-agent",
|
||||
Version: "1.0.0",
|
||||
IntervalSeconds: 30,
|
||||
},
|
||||
Host: agentsdocker.HostInfo{
|
||||
Hostname: "swarm-node-a",
|
||||
MachineID: "",
|
||||
},
|
||||
Timestamp: baseTime,
|
||||
}
|
||||
|
||||
host, err := monitor.ApplyDockerReport(firstReport, token)
|
||||
if err != nil {
|
||||
t.Fatalf("first ApplyDockerReport failed: %v", err)
|
||||
}
|
||||
if host.ID == "" {
|
||||
t.Fatal("expected first host id")
|
||||
}
|
||||
|
||||
secondReport := firstReport
|
||||
secondReport.Host.Hostname = "swarm-node-b"
|
||||
secondReport.Timestamp = baseTime.Add(30 * time.Second)
|
||||
|
||||
_, err = monitor.ApplyDockerReport(secondReport, token)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for shared token reused across different hosts")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Generate a dedicated token") {
|
||||
t.Fatalf("expected dedicated token guidance, got: %v", err)
|
||||
}
|
||||
|
||||
hosts := monitor.state.GetDockerHosts()
|
||||
if len(hosts) != 1 {
|
||||
t.Fatalf("expected original host state to remain intact, got %d hosts", len(hosts))
|
||||
}
|
||||
if hosts[0].Hostname != "swarm-node-a" {
|
||||
t.Fatalf("expected original host to remain swarm-node-a, got %q", hosts[0].Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDockerReport_MissingHostname(t *testing.T) {
|
||||
monitor := newTestMonitor(t)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue