Files
rdpgw/cmd/auth/peercred_linux.go
bolkedebruin de31bfe8a0 Restrict the rdpgw-auth socket to its own UID by default (#190)
The auth daemon's gRPC socket was world-writable and accepted any
local UID that could connect to it. On a multi-tenant host any user
on the box could speak the gRPC API and run an arbitrary username/
password through PAM -- effectively an unauthenticated PAM oracle.

Create the socket with mode 0660 (Umask(0117)) and gate Accept on
SO_PEERCRED: only the daemon's own UID is allowed by default, plus
any operator-supplied --allow-uid / --allow-gid. Privilege-separated
deployments (rdpgw and rdpgw-auth as different users) need to list
the gateway's UID, or share a group; the existing path otherwise
would have been permissive.

The peer-credentials check is Linux-only; the non-Linux build keeps
the listener as-is and logs a warning, since rdpgw-auth itself
requires libpam and is effectively Linux-only in practice.
2026-04-30 18:59:48 +02:00

90 lines
2.0 KiB
Go

//go:build linux
package main
import (
"fmt"
"log"
"net"
"golang.org/x/sys/unix"
)
// gatedListener wraps a unix-socket net.Listener and accepts only
// connections whose peer UID/GID is on the allow-list. The check is
// applied at Accept(), before any application data is read, so the
// gRPC server never sees an unauthorized caller.
type gatedListener struct {
net.Listener
allowedUIDs map[uint32]struct{}
allowedGIDs map[uint32]struct{}
}
func newGatedListener(l net.Listener, uids []int, gids []int) net.Listener {
uidSet := make(map[uint32]struct{}, len(uids))
for _, u := range uids {
uidSet[uint32(u)] = struct{}{}
}
gidSet := make(map[uint32]struct{}, len(gids))
for _, g := range gids {
gidSet[uint32(g)] = struct{}{}
}
return &gatedListener{Listener: l, allowedUIDs: uidSet, allowedGIDs: gidSet}
}
func (l *gatedListener) Accept() (net.Conn, error) {
for {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
ucred, err := peerCred(conn)
if err != nil {
log.Printf("rejecting connection: cannot read peer credentials: %s", err)
conn.Close()
continue
}
if !l.allowed(ucred) {
log.Printf("rejecting connection from uid=%d gid=%d pid=%d", ucred.Uid, ucred.Gid, ucred.Pid)
conn.Close()
continue
}
return conn, nil
}
}
func (l *gatedListener) allowed(c *unix.Ucred) bool {
if _, ok := l.allowedUIDs[c.Uid]; ok {
return true
}
if _, ok := l.allowedGIDs[c.Gid]; ok {
return true
}
return false
}
func peerCred(conn net.Conn) (*unix.Ucred, error) {
uc, ok := conn.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("connection is not a *net.UnixConn (got %T)", conn)
}
raw, err := uc.SyscallConn()
if err != nil {
return nil, err
}
var (
ucred *unix.Ucred
credErr error
)
ctrlErr := raw.Control(func(fd uintptr) {
ucred, credErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
})
if ctrlErr != nil {
return nil, ctrlErr
}
if credErr != nil {
return nil, credErr
}
return ucred, nil
}