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:
|
**In containers**, direct SSH is blocked:
|
||||||
```go
|
```go
|
||||||
if isRunningInContainer() && !devModeAllowSSH {
|
if system.InContainer() && !devModeAllowSSH {
|
||||||
log.Error().Msg("SECURITY BLOCK: SSH temperature collection disabled in containers")
|
log.Error().Msg("SECURITY BLOCK: SSH temperature collection disabled in containers")
|
||||||
return &Temperature{Available: false}, nil
|
return &Temperature{Available: false}, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Container Detection Methods**:
|
**Container Detection Methods**:
|
||||||
1. Check for `/.dockerenv` file
|
1. `PULSE_FORCE_CONTAINER=1` override for explicit opt-in
|
||||||
2. Check `/proc/1/cgroup` for "docker", "lxc", "containerd"
|
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))
|
**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/mock"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
"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/tempproxy"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
pkgdiscovery "github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
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()))
|
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
|
// generateNodeID creates a unique ID for a node
|
||||||
func generateNodeID(nodeType string, index int) string {
|
func generateNodeID(nodeType string, index int) string {
|
||||||
return fmt.Sprintf("%s-%d", nodeType, index)
|
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)
|
// CRITICAL SECURITY CHECK: Never generate SSH keys in containers (unless dev mode)
|
||||||
// Container compromise = SSH key compromise = root access to Proxmox
|
// Container compromise = SSH key compromise = root access to Proxmox
|
||||||
devModeAllowSSH := os.Getenv("PULSE_DEV_ALLOW_CONTAINER_SSH") == "true"
|
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("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("For temperature monitoring in containers, deploy pulse-sensor-proxy on the Proxmox host")
|
||||||
log.Error().Msg("See: https://docs.pulseapp.io/security/containerized-deployments")
|
log.Error().Msg("See: https://docs.pulseapp.io/security/containerized-deployments")
|
||||||
|
|
@ -5587,7 +5569,7 @@ func (h *ConfigHandlers) getOrGenerateSSHKeys() SSHKeyPair {
|
||||||
return 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("⚠️ 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!")
|
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/mock"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
"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/types"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
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
|
// in a container with SSH-based temperature monitoring enabled
|
||||||
func checkContainerizedTempMonitoring() {
|
func checkContainerizedTempMonitoring() {
|
||||||
// Check if running in container
|
// Check if running in container
|
||||||
isContainer := os.Getenv("PULSE_DOCKER") == "true" || isRunningInContainer()
|
isContainer := os.Getenv("PULSE_DOCKER") == "true" || system.InContainer()
|
||||||
if !isContainer {
|
if !isContainer {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -3002,27 +3003,6 @@ func checkContainerizedTempMonitoring() {
|
||||||
"See documentation for hardening recommendations.")
|
"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
|
// New creates a new Monitor instance
|
||||||
func New(cfg *config.Config) (*Monitor, error) {
|
func New(cfg *config.Config) (*Monitor, error) {
|
||||||
// Initialize temperature collector with sensors SSH key
|
// 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/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ssh/knownhosts"
|
"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/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
||||||
"github.com/rs/zerolog/log"
|
"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)
|
// SECURITY: Block SSH fallback when running in containers (unless dev mode)
|
||||||
// Container compromise = SSH key compromise = root access to infrastructure
|
// Container compromise = SSH key compromise = root access to infrastructure
|
||||||
devModeAllowSSH := os.Getenv("PULSE_DEV_ALLOW_CONTAINER_SSH") == "true"
|
devModeAllowSSH := os.Getenv("PULSE_DEV_ALLOW_CONTAINER_SSH") == "true"
|
||||||
if isRunningInContainer() && !devModeAllowSSH {
|
if system.InContainer() && !devModeAllowSSH {
|
||||||
log.Error().
|
log.Error().
|
||||||
Str("node", nodeName).
|
Str("node", nodeName).
|
||||||
Msg("SECURITY BLOCK: SSH temperature collection disabled in containers - deploy pulse-sensor-proxy")
|
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
|
var proxyErr *tempproxy.ProxyError
|
||||||
if errors.As(err, &proxyErr) {
|
if errors.As(err, &proxyErr) {
|
||||||
switch proxyErr.Type {
|
switch proxyErr.Type {
|
||||||
case tempproxy.ErrorTypeTransport, tempproxy.ErrorTypeTimeout:
|
case tempproxy.ErrorTypeTransport, tempproxy.ErrorTypeTimeout:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
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