Enhance container detection for temperature SSH safeguards (refs #601)

This commit is contained in:
rcourtman 2025-11-04 22:30:35 +00:00
parent adda6eea38
commit 10862db4e4
5 changed files with 80 additions and 49 deletions

View file

@ -193,15 +193,17 @@ from="192.168.0.0/24,10.0.0.0/8"
**In containers**, direct SSH is blocked:
```go
if isRunningInContainer() && !devModeAllowSSH {
if system.InContainer() && !devModeAllowSSH {
log.Error().Msg("SECURITY BLOCK: SSH temperature collection disabled in containers")
return &Temperature{Available: false}, nil
}
```
**Container Detection Methods**:
1. Check for `/.dockerenv` file
2. Check `/proc/1/cgroup` for "docker", "lxc", "containerd"
1. `PULSE_FORCE_CONTAINER=1` override for explicit opt-in
2. Presence of `/.dockerenv` or `/run/.containerenv`
3. `container=` hints from environment variables
4. `/proc/1/environ` and `/proc/1/cgroup` markers (`docker`, `lxc`, `containerd`, `kubepods`, etc.)
**Bypass**: Only possible with explicit environment variable (see [Development Mode](#development-mode))

View file

@ -28,6 +28,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/system"
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
@ -2664,25 +2665,6 @@ func (h *ConfigHandlers) HandleVerifyTemperatureSSH(w http.ResponseWriter, r *ht
w.Write([]byte(response.String()))
}
// isRunningInContainer detects if Pulse is running inside a container
func isRunningInContainer() bool {
// Check for /.dockerenv file
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check cgroup for container indicators
data, err := os.ReadFile("/proc/1/cgroup")
if err == nil {
content := string(data)
if strings.Contains(content, "docker") || strings.Contains(content, "lxc") || strings.Contains(content, "containerd") {
return true
}
}
return false
}
// generateNodeID creates a unique ID for a node
func generateNodeID(nodeType string, index int) string {
return fmt.Sprintf("%s-%d", nodeType, index)
@ -5579,7 +5561,7 @@ func (h *ConfigHandlers) getOrGenerateSSHKeys() SSHKeyPair {
// CRITICAL SECURITY CHECK: Never generate SSH keys in containers (unless dev mode)
// Container compromise = SSH key compromise = root access to Proxmox
devModeAllowSSH := os.Getenv("PULSE_DEV_ALLOW_CONTAINER_SSH") == "true"
if isRunningInContainer() && !devModeAllowSSH {
if system.InContainer() && !devModeAllowSSH {
log.Error().Msg("SECURITY BLOCK: SSH key generation disabled in containerized deployments")
log.Error().Msg("For temperature monitoring in containers, deploy pulse-sensor-proxy on the Proxmox host")
log.Error().Msg("See: https://docs.pulseapp.io/security/containerized-deployments")
@ -5587,7 +5569,7 @@ func (h *ConfigHandlers) getOrGenerateSSHKeys() SSHKeyPair {
return SSHKeyPair{}
}
if devModeAllowSSH && isRunningInContainer() {
if devModeAllowSSH && system.InContainer() {
log.Warn().Msg("⚠️ DEV MODE: SSH key generation ENABLED in container - FOR TESTING ONLY")
log.Warn().Msg("⚠️ This grants root SSH access from container - NEVER use in production!")
}

View file

@ -29,6 +29,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
"github.com/rcourtman/pulse-go-rewrite/internal/system"
"github.com/rcourtman/pulse-go-rewrite/internal/types"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
@ -2978,7 +2979,7 @@ func (m *Monitor) GetConnectionStatuses() map[string]bool {
// in a container with SSH-based temperature monitoring enabled
func checkContainerizedTempMonitoring() {
// Check if running in container
isContainer := os.Getenv("PULSE_DOCKER") == "true" || isRunningInContainer()
isContainer := os.Getenv("PULSE_DOCKER") == "true" || system.InContainer()
if !isContainer {
return
}
@ -3002,27 +3003,6 @@ func checkContainerizedTempMonitoring() {
"See documentation for hardening recommendations.")
}
// isRunningInContainer detects if running inside a container
func isRunningInContainer() bool {
// Check for /.dockerenv
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check cgroup for container indicators
data, err := os.ReadFile("/proc/1/cgroup")
if err == nil {
content := string(data)
if strings.Contains(content, "docker") ||
strings.Contains(content, "lxc") ||
strings.Contains(content, "containerd") {
return true
}
}
return false
}
// New creates a new Monitor instance
func New(cfg *config.Config) (*Monitor, error) {
// Initialize temperature collector with sensors SSH key

View file

@ -17,6 +17,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/ssh/knownhosts"
"github.com/rcourtman/pulse-go-rewrite/internal/system"
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
"github.com/rs/zerolog/log"
)
@ -102,7 +103,7 @@ func (tc *TemperatureCollector) CollectTemperature(ctx context.Context, nodeHost
// SECURITY: Block SSH fallback when running in containers (unless dev mode)
// Container compromise = SSH key compromise = root access to infrastructure
devModeAllowSSH := os.Getenv("PULSE_DEV_ALLOW_CONTAINER_SSH") == "true"
if isRunningInContainer() && !devModeAllowSSH {
if system.InContainer() && !devModeAllowSSH {
log.Error().
Str("node", nodeName).
Msg("SECURITY BLOCK: SSH temperature collection disabled in containers - deploy pulse-sensor-proxy")
@ -600,8 +601,8 @@ func (tc *TemperatureCollector) shouldDisableProxy(err error) bool {
var proxyErr *tempproxy.ProxyError
if errors.As(err, &proxyErr) {
switch proxyErr.Type {
case tempproxy.ErrorTypeTransport, tempproxy.ErrorTypeTimeout:
return true
case tempproxy.ErrorTypeTransport, tempproxy.ErrorTypeTimeout:
return true
default:
return false
}

View file

@ -0,0 +1,66 @@
package system
import (
"os"
"strings"
)
var containerMarkers = []string{
"docker",
"lxc",
"containerd",
"kubepods",
"podman",
"crio",
"libpod",
"lxcfs",
}
// InContainer reports whether Pulse is running inside a containerised environment.
func InContainer() bool {
// Allow operators to force container behaviour when automatic detection falls short.
if isTruthy(os.Getenv("PULSE_FORCE_CONTAINER")) {
return true
}
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
if _, err := os.Stat("/run/.containerenv"); err == nil {
return true
}
// Check common environment hints provided by systemd/nspawn, LXC, etc.
if val := strings.ToLower(strings.TrimSpace(os.Getenv("container"))); val != "" && val != "host" {
return true
}
// Some distros expose the container hint through PID 1's environment.
if data, err := os.ReadFile("/proc/1/environ"); err == nil {
lower := strings.ToLower(string(data))
if strings.Contains(lower, "container=") && !strings.Contains(lower, "container=host") {
return true
}
}
// Fall back to cgroup inspection which covers older Docker/LXC setups.
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
content := strings.ToLower(string(data))
for _, marker := range containerMarkers {
if strings.Contains(content, marker) {
return true
}
}
}
return false
}
func isTruthy(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "t", "yes", "y", "on":
return true
default:
return false
}
}