mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
Automate sensor proxy container mount and auth
This commit is contained in:
parent
156fd34c50
commit
e4c3b06f14
7 changed files with 467 additions and 75 deletions
|
|
@ -1,8 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -15,10 +19,18 @@ type peerCredentials struct {
|
|||
gid uint32
|
||||
}
|
||||
|
||||
// extractPeerCredentials extracts and verifies peer credentials
|
||||
// Returns credentials if authorized, error otherwise
|
||||
type idRange struct {
|
||||
start uint64
|
||||
length uint64
|
||||
}
|
||||
|
||||
func (r idRange) contains(v uint32) bool {
|
||||
value := uint64(v)
|
||||
return value >= r.start && value < r.start+r.length
|
||||
}
|
||||
|
||||
// extractPeerCredentials extracts peer credentials via SO_PEERCRED
|
||||
func extractPeerCredentials(conn net.Conn) (*peerCredentials, error) {
|
||||
// Get the underlying file descriptor
|
||||
unixConn, ok := conn.(*net.UnixConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a unix connection")
|
||||
|
|
@ -32,7 +44,6 @@ func extractPeerCredentials(conn net.Conn) (*peerCredentials, error) {
|
|||
|
||||
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 nil, fmt.Errorf("failed to get peer credentials: %w", err)
|
||||
|
|
@ -44,31 +55,201 @@ func extractPeerCredentials(conn net.Conn) (*peerCredentials, error) {
|
|||
Uint32("gid", cred.Gid).
|
||||
Msg("Peer credentials")
|
||||
|
||||
// Allow root (UID 0) - this covers most service scenarios
|
||||
if cred.Uid == 0 {
|
||||
return &peerCredentials{
|
||||
uid: cred.Uid,
|
||||
pid: uint32(cred.Pid),
|
||||
gid: cred.Gid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Allow the proxy's own user (for testing/debugging)
|
||||
if cred.Uid == uint32(syscall.Getuid()) {
|
||||
return &peerCredentials{
|
||||
uid: cred.Uid,
|
||||
pid: uint32(cred.Pid),
|
||||
gid: cred.Gid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Reject all other users
|
||||
return nil, fmt.Errorf("unauthorized: uid=%d gid=%d", cred.Uid, cred.Gid)
|
||||
return &peerCredentials{
|
||||
uid: cred.Uid,
|
||||
pid: uint32(cred.Pid),
|
||||
gid: cred.Gid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// verifyPeerCredentials checks if the connecting process is authorized (legacy function)
|
||||
// Returns nil if authorized, error otherwise
|
||||
func verifyPeerCredentials(conn net.Conn) error {
|
||||
_, err := extractPeerCredentials(conn)
|
||||
return err
|
||||
// initAuthRules builds in-memory allow lists for peer validation
|
||||
func (p *Proxy) initAuthRules() error {
|
||||
p.allowedPeerUIDs = make(map[uint32]struct{})
|
||||
p.allowedPeerGIDs = make(map[uint32]struct{})
|
||||
|
||||
// Always allow root and the proxy's own user
|
||||
p.allowedPeerUIDs[0] = struct{}{}
|
||||
p.allowedPeerUIDs[uint32(os.Getuid())] = struct{}{}
|
||||
p.allowedPeerGIDs[0] = struct{}{}
|
||||
p.allowedPeerGIDs[uint32(os.Getgid())] = struct{}{}
|
||||
|
||||
if len(p.config.AllowedPeerUIDs) > 0 {
|
||||
for _, uid := range dedupeUint32(p.config.AllowedPeerUIDs) {
|
||||
p.allowedPeerUIDs[uid] = struct{}{}
|
||||
}
|
||||
log.Info().
|
||||
Int("explicit_uid_allow_count", len(p.config.AllowedPeerUIDs)).
|
||||
Msg("Loaded explicit peer UID allow-list entries")
|
||||
}
|
||||
|
||||
if len(p.config.AllowedPeerGIDs) > 0 {
|
||||
for _, gid := range dedupeUint32(p.config.AllowedPeerGIDs) {
|
||||
p.allowedPeerGIDs[gid] = struct{}{}
|
||||
}
|
||||
log.Info().
|
||||
Int("explicit_gid_allow_count", len(p.config.AllowedPeerGIDs)).
|
||||
Msg("Loaded explicit peer GID allow-list entries")
|
||||
}
|
||||
|
||||
if !p.config.AllowIDMappedRoot {
|
||||
log.Info().Msg("ID-mapped root authentication disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
users := dedupeStrings(p.config.AllowedIDMapUsers)
|
||||
if len(users) == 0 {
|
||||
users = []string{"root"}
|
||||
}
|
||||
|
||||
uidRanges, err := loadSubIDRanges("/etc/subuid", users)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading subordinate UID ranges: %w", err)
|
||||
}
|
||||
gidRanges, err := loadSubIDRanges("/etc/subgid", users)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading subordinate GID ranges: %w", err)
|
||||
}
|
||||
|
||||
p.idMappedUIDRanges = uidRanges
|
||||
p.idMappedGIDRanges = gidRanges
|
||||
|
||||
if len(uidRanges) == 0 || len(gidRanges) == 0 {
|
||||
log.Warn().
|
||||
Strs("users", users).
|
||||
Msg("allow_idmapped_root enabled but no subordinate ID ranges detected; LXC connections may fail")
|
||||
} else {
|
||||
log.Info().
|
||||
Int("uid_range_count", len(uidRanges)).
|
||||
Int("gid_range_count", len(gidRanges)).
|
||||
Strs("users", users).
|
||||
Msg("Loaded subordinate ID ranges for ID-mapped root authentication")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizePeer verifies the peer credentials against configured allow lists
|
||||
func (p *Proxy) authorizePeer(cred *peerCredentials) error {
|
||||
if _, ok := p.allowedPeerUIDs[cred.uid]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.config.AllowIDMappedRoot && p.isIDMappedRoot(cred) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unauthorized: uid=%d gid=%d", cred.uid, cred.gid)
|
||||
}
|
||||
|
||||
func (p *Proxy) isIDMappedRoot(cred *peerCredentials) bool {
|
||||
if len(p.idMappedUIDRanges) == 0 || len(p.idMappedGIDRanges) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !rangeContains(p.idMappedUIDRanges, cred.uid) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !rangeContains(p.idMappedGIDRanges, cred.gid) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func rangeContains(ranges []idRange, value uint32) bool {
|
||||
for _, r := range ranges {
|
||||
if r.contains(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func dedupeUint32(values []uint32) []uint32 {
|
||||
seen := make(map[uint32]struct{})
|
||||
var result []uint32
|
||||
for _, val := range values {
|
||||
if _, ok := seen[val]; ok {
|
||||
continue
|
||||
}
|
||||
seen[val] = struct{}{}
|
||||
result = append(result, val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func dedupeStrings(values []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var result []string
|
||||
for _, val := range values {
|
||||
trimmed := strings.TrimSpace(val)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadSubIDRanges(path string, users []string) ([]idRange, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userFilter := make(map[string]struct{}, len(users))
|
||||
for _, user := range users {
|
||||
userFilter[user] = struct{}{}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var ranges []idRange
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(userFilter) > 0 {
|
||||
if _, ok := userFilter[parts[0]]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
start, err := strconv.ParseUint(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Str("entry", line).
|
||||
Err(err).
|
||||
Msg("Skipping subordinate ID entry with invalid start value")
|
||||
continue
|
||||
}
|
||||
|
||||
length, err := strconv.ParseUint(parts[2], 10, 64)
|
||||
if err != nil || length == 0 {
|
||||
log.Warn().
|
||||
Str("entry", line).
|
||||
Err(err).
|
||||
Msg("Skipping subordinate ID entry with invalid length")
|
||||
continue
|
||||
}
|
||||
|
||||
ranges = append(ranges, idRange{start: start, length: length})
|
||||
}
|
||||
|
||||
return ranges, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue