diff --git a/cmd/pulse-sensor-proxy/ssh.go b/cmd/pulse-sensor-proxy/ssh.go index 1de3c5b..faf45cd 100644 --- a/cmd/pulse-sensor-proxy/ssh.go +++ b/cmd/pulse-sensor-proxy/ssh.go @@ -580,27 +580,31 @@ func discoverClusterNodes() ([]string, error) { stdoutStr := out.String() combinedOutput := stderrStr + stdoutStr - // Broadly detect standalone/container scenarios by checking for known patterns - // This list comes from real user reports and should handle localization/version differences - // - // Common patterns that indicate standalone/container operation: - // - Configuration missing: "does not exist", "not found", "no such file" - // - Cluster state: "not part of a cluster", "no cluster", "standalone" - // - IPC failures: "ipcc_send_rec", "IPC", "communication failed" - // - Permission/access: "Unknown error -1", "Unable to load", "access denied", "permission denied" - // - // Strategy: Be permissive - if pvecm fails with any of these common patterns, - // assume standalone and fall back to localhost. This is safer than false negatives. + // First check for IPC/permission errors - these indicate a cluster exists but we can't access it + // These should NOT be treated as standalone mode + ipcErrorIndicators := []string{ + "ipcc_send_rec", + "Unable to load access control list", + "access control list", + } + + for _, indicator := range ipcErrorIndicators { + if strings.Contains(combinedOutput, indicator) { + log.Warn(). + Str("stderr", stderrStr). + Msg("Cannot access Proxmox cluster IPC - cluster validation disabled. Add nodes to allowed_nodes in config if needed.") + // Return error to disable cluster validation rather than falling back to incorrect standalone mode + return nil, fmt.Errorf("pvecm cluster IPC access denied (check systemd restrictions or run outside container): %s", stderrStr) + } + } + + // Now check for true standalone/no-cluster patterns + // These indicate the node genuinely isn't part of a cluster standaloneIndicators := []string{ - // Configuration issues + // Configuration missing "does not exist", "not found", "no such file", // Cluster state "not part of a cluster", "no cluster", "standalone", - // IPC/communication failures (common in LXC) - "ipcc_send_rec", "IPC", "communication failed", "connection refused", - // Permission/access issues - "Unknown error -1", "Unable to load", "access denied", "permission denied", - "access control list", } isStandalone := false @@ -612,10 +616,10 @@ func discoverClusterNodes() ([]string, error) { } if isStandalone { - // Log at INFO level since this is expected for standalone/container scenarios + // Log at INFO level since this is expected for standalone scenarios log.Info(). Str("exit_code", fmt.Sprintf("%v", err)). - Msg("Standalone Proxmox node or LXC container detected - using localhost for temperature collection") + Msg("Standalone Proxmox node detected - discovering local host addresses for validation") return discoverLocalHostAddresses() } diff --git a/cmd/pulse-sensor-proxy/validation.go b/cmd/pulse-sensor-proxy/validation.go index 6d91046..2016dcc 100644 --- a/cmd/pulse-sensor-proxy/validation.go +++ b/cmd/pulse-sensor-proxy/validation.go @@ -357,13 +357,19 @@ func (v *nodeValidator) Validate(ctx context.Context, node string) error { if v.clusterEnabled { allowed, err := v.matchesCluster(ctx, node) if err != nil { + // Cluster query failed (e.g., IPC permission denied, running in LXC) + // Fall through to permissive mode rather than blocking all requests v.recordFailure(validationReasonClusterFailed) - return fmt.Errorf("failed to evaluate cluster membership: %w", err) - } - if !allowed { + log.Debug(). + Err(err). + Str("node", node). + Msg("Cluster validation unavailable, allowing request (consider configuring allowed_nodes for security)") + // Fall through to permissive mode below + } else if !allowed { return v.deny(node, validationReasonNotClusterMember) + } else { + return nil } - return nil } if v.strict { diff --git a/scripts/install-sensor-proxy.sh b/scripts/install-sensor-proxy.sh index 175387b..ae45ff7 100755 --- a/scripts/install-sensor-proxy.sh +++ b/scripts/install-sensor-proxy.sh @@ -715,6 +715,7 @@ NoNewPrivileges=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/var/lib/pulse-sensor-proxy +ReadWritePaths=-/run/corosync ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true @@ -724,7 +725,6 @@ PrivateDevices=true ProtectProc=invisible ProcSubset=pid LockPersonality=true -RemoveIPC=true RestrictSUIDSGID=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK RestrictNamespaces=true @@ -774,6 +774,7 @@ NoNewPrivileges=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/var/lib/pulse-sensor-proxy +ReadWritePaths=-/run/corosync ReadOnlyPaths=/run/pve-cluster /etc/pve ProtectKernelTunables=true ProtectKernelModules=true @@ -784,7 +785,6 @@ PrivateDevices=true ProtectProc=invisible ProcSubset=pid LockPersonality=true -RemoveIPC=true RestrictSUIDSGID=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK RestrictNamespaces=true diff --git a/scripts/pulse-sensor-proxy.service b/scripts/pulse-sensor-proxy.service index b39172a..aad44d7 100644 --- a/scripts/pulse-sensor-proxy.service +++ b/scripts/pulse-sensor-proxy.service @@ -22,6 +22,7 @@ NoNewPrivileges=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/var/lib/pulse-sensor-proxy +ReadWritePaths=-/run/corosync ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true @@ -31,7 +32,6 @@ PrivateDevices=true ProtectProc=invisible ProcSubset=pid LockPersonality=true -RemoveIPC=true RestrictSUIDSGID=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK RestrictNamespaces=true