diff --git a/cmd/pulse-temp-proxy/auth.go b/cmd/pulse-temp-proxy/auth.go new file mode 100644 index 0000000..0cc4c91 --- /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 b167061..5fa1f65 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 73ad92f..990d6a8 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)