From 6d4694f019353ea72f3bd962a49e3efbd12f171f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Oct 2025 21:42:22 +0000 Subject: [PATCH] security: Add SO_PEERCRED authentication to temperature proxy Addresses security concern raised in code review: - Socket permissions changed from 0666 to 0660 - Added SO_PEERCRED verification to authenticate connecting processes - Only allows root (UID 0) or proxy's own user - Prevents unauthorized processes from triggering SSH key rollout - Documented passwordless root SSH requirement for clusters This prevents any process on the host or in other containers from accessing the proxy RPC endpoints. --- cmd/pulse-temp-proxy/auth.go | 52 ++++++++++++++++++++++++++++++++++ cmd/pulse-temp-proxy/main.go | 12 ++++++-- docs/TEMPERATURE_MONITORING.md | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 cmd/pulse-temp-proxy/auth.go diff --git a/cmd/pulse-temp-proxy/auth.go b/cmd/pulse-temp-proxy/auth.go new file mode 100644 index 000000000..0cc4c914f --- /dev/null +++ b/cmd/pulse-temp-proxy/auth.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "net" + "syscall" + + "github.com/rs/zerolog/log" +) + +// verifyPeerCredentials checks if the connecting process is authorized +// Returns nil if authorized, error otherwise +func verifyPeerCredentials(conn net.Conn) error { + // Get the underlying file descriptor + unixConn, ok := conn.(*net.UnixConn) + if !ok { + return fmt.Errorf("not a unix connection") + } + + file, err := unixConn.File() + if err != nil { + return fmt.Errorf("failed to get file descriptor: %w", err) + } + defer file.Close() + + fd := int(file.Fd()) + + // Get peer credentials using SO_PEERCRED + cred, err := syscall.GetsockoptUcred(fd, syscall.SOL_SOCKET, syscall.SO_PEERCRED) + if err != nil { + return fmt.Errorf("failed to get peer credentials: %w", err) + } + + log.Debug(). + Int32("pid", cred.Pid). + Uint32("uid", cred.Uid). + Uint32("gid", cred.Gid). + Msg("Peer credentials") + + // Allow root (UID 0) - this covers most service scenarios + if cred.Uid == 0 { + return nil + } + + // Allow the proxy's own user (for testing/debugging) + if cred.Uid == uint32(syscall.Getuid()) { + return nil + } + + // Reject all other users + return fmt.Errorf("unauthorized: uid=%d gid=%d", cred.Uid, cred.Gid) +} diff --git a/cmd/pulse-temp-proxy/main.go b/cmd/pulse-temp-proxy/main.go index b1670610a..5fa1f6531 100644 --- a/cmd/pulse-temp-proxy/main.go +++ b/cmd/pulse-temp-proxy/main.go @@ -158,8 +158,9 @@ func (p *Proxy) Start() error { } p.listener = listener - // Set socket permissions so Pulse container can access it - if err := os.Chmod(p.socketPath, 0666); err != nil { + // Set socket permissions to owner+group only + // We use SO_PEERCRED for authentication, so we don't need world-readable + if err := os.Chmod(p.socketPath, 0660); err != nil { log.Warn().Err(err).Msg("Failed to set socket permissions") } @@ -200,6 +201,13 @@ func (p *Proxy) acceptConnections() { func (p *Proxy) handleConnection(conn net.Conn) { defer conn.Close() + // Verify peer credentials (SO_PEERCRED authentication) + if err := verifyPeerCredentials(conn); err != nil { + log.Warn().Err(err).Msg("Unauthorized connection attempt") + p.sendError(conn, "unauthorized") + return + } + // Decode request var req RPCRequest decoder := json.NewDecoder(conn) diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 73ad92fc5..990d6a88e 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -43,6 +43,7 @@ For native (non-containerized) installations, Pulse connects directly via SSH: 1. **SSH Key Authentication**: Your Pulse server needs SSH key access to nodes (no passwords) 2. **lm-sensors Package**: Installed on nodes to read hardware sensors +3. **Passwordless root SSH** (Proxmox clusters only): For proxy architecture, the Proxmox host running Pulse must have passwordless root SSH access to all cluster nodes. This is standard for Proxmox clusters but hardened environments may need to create an alternate service account. ## Setup (Automatic)