Enhance container detection for temperature SSH safeguards (refs #601)
This commit is contained in:
parent
adda6eea38
commit
10862db4e4
5 changed files with 80 additions and 49 deletions
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
66
internal/system/container.go
Normal file
66
internal/system/container.go
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue