mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 21:10:13 +00:00
feat(security): Add capability-based authorization
Implements proper least-privilege model for RPC methods. Previously,
any UID in allowed_peer_uids could call privileged methods, meaning
another service's UID would inherit full host-level control.
Capability System:
- Three levels: read, write, admin
- Per-UID capability assignment via allowed_peers config
- Privileged methods require admin capability
- Backwards compatible with legacy allowed_peer_uids format
Configuration:
allowed_peers:
- uid: 0
capabilities: [read, write, admin] # Root gets all
- uid: 1000
capabilities: [read] # Docker: read-only
- uid: 1001
capabilities: [read, write] # Temps but not key distribution
Security benefit: Services can be granted only the capabilities they
need, preventing unintended privilege escalation.
Related to security audit 2025-11-07.
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
734cebb4dc
commit
9aafa6449f
2 changed files with 46 additions and 16 deletions
35
cmd/pulse-sensor-proxy/capabilities.go
Normal file
35
cmd/pulse-sensor-proxy/capabilities.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// Capability represents a permission bit granted to a peer.
|
||||
type Capability uint32
|
||||
|
||||
const (
|
||||
CapabilityRead Capability = 1 << iota
|
||||
CapabilityWrite
|
||||
CapabilityAdmin
|
||||
capabilityLegacyAll = CapabilityRead | CapabilityWrite | CapabilityAdmin
|
||||
)
|
||||
|
||||
func (c Capability) Has(flag Capability) bool {
|
||||
return c&flag == flag
|
||||
}
|
||||
|
||||
func parseCapabilityList(values []string) Capability {
|
||||
if len(values) == 0 {
|
||||
return CapabilityRead
|
||||
}
|
||||
var caps Capability
|
||||
for _, raw := range values {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "read":
|
||||
caps |= CapabilityRead
|
||||
case "write":
|
||||
caps |= CapabilityWrite
|
||||
case "admin":
|
||||
caps |= CapabilityAdmin
|
||||
}
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ func TestPrivilegedMethodsBlocked(t *testing.T) {
|
|||
config: &Config{AllowIDMappedRoot: true},
|
||||
allowedPeerUIDs: map[uint32]struct{}{0: {}},
|
||||
allowedPeerGIDs: map[uint32]struct{}{0: {}},
|
||||
peerCapabilities: map[uint32]Capability{0: capabilityLegacyAll},
|
||||
idMappedUIDRanges: []idRange{{start: 100000, length: 65536}},
|
||||
idMappedGIDRanges: []idRange{{start: 100000, length: 65536}},
|
||||
}
|
||||
|
|
@ -60,33 +61,26 @@ func TestPrivilegedMethodsBlocked(t *testing.T) {
|
|||
// Test that containers ARE blocked from privileged methods
|
||||
t.Run("ContainerBlockedFromPrivilegedMethods", func(t *testing.T) {
|
||||
// Container should pass authentication
|
||||
if err := p.authorizePeer(containerCreds); err != nil {
|
||||
caps, err := p.authorizePeer(containerCreds)
|
||||
if err != nil {
|
||||
t.Fatalf("Container should pass authentication, got: %v", err)
|
||||
}
|
||||
|
||||
// But should be identified as ID-mapped root
|
||||
if !p.isIDMappedRoot(containerCreds) {
|
||||
t.Fatal("Container credentials should be identified as ID-mapped root")
|
||||
}
|
||||
|
||||
// Test all privileged methods are blocked for containers
|
||||
for method := range privilegedMethods {
|
||||
if !p.isIDMappedRoot(containerCreds) {
|
||||
t.Errorf("Container should be blocked from %s", method)
|
||||
}
|
||||
if caps.Has(CapabilityAdmin) {
|
||||
t.Fatal("Container should not have admin capability")
|
||||
}
|
||||
})
|
||||
|
||||
// Test that host CAN call privileged methods
|
||||
t.Run("HostAllowedPrivilegedMethods", func(t *testing.T) {
|
||||
// Host should pass authentication
|
||||
if err := p.authorizePeer(hostCreds); err != nil {
|
||||
caps, err := p.authorizePeer(hostCreds)
|
||||
if err != nil {
|
||||
t.Fatalf("Host should pass authentication, got: %v", err)
|
||||
}
|
||||
|
||||
// Host should NOT be identified as ID-mapped root
|
||||
if p.isIDMappedRoot(hostCreds) {
|
||||
t.Fatal("Host credentials should NOT be identified as ID-mapped root")
|
||||
if !caps.Has(CapabilityAdmin) {
|
||||
t.Fatal("Host should have admin capability")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -177,6 +171,7 @@ func TestIDMappedRootDisabled(t *testing.T) {
|
|||
p := &Proxy{
|
||||
config: &Config{AllowIDMappedRoot: false},
|
||||
allowedPeerUIDs: map[uint32]struct{}{0: {}},
|
||||
peerCapabilities: map[uint32]Capability{0: capabilityLegacyAll},
|
||||
idMappedUIDRanges: []idRange{{start: 100000, length: 65536}},
|
||||
idMappedGIDRanges: []idRange{{start: 100000, length: 65536}},
|
||||
}
|
||||
|
|
@ -185,7 +180,7 @@ func TestIDMappedRootDisabled(t *testing.T) {
|
|||
cred := &peerCredentials{uid: 110000, gid: 110000}
|
||||
|
||||
// Should fail authorization when AllowIDMappedRoot is false
|
||||
if err := p.authorizePeer(cred); err == nil {
|
||||
if _, err := p.authorizePeer(cred); err == nil {
|
||||
t.Error("authorizePeer should fail for ID-mapped root when AllowIDMappedRoot is false")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue