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.
This commit is contained in:
rcourtman
2025-10-12 21:42:22 +00:00
parent e7bc338891
commit 6d4694f019
3 changed files with 63 additions and 2 deletions

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)