diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 20fb99ca0..c77ab7a8a 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -173,3 +173,126 @@ You can still manage the entry manually if you prefer, but no extra steps are re - Timeout: 5 seconds (non-blocking) - Falls back gracefully if SSH fails - No impact if SSH is not configured + +## Container Security Considerations + +⚠️ **Important for Docker/LXC deployments** + +If you run Pulse in a container (Docker or LXC), SSH private keys are stored inside the container filesystem. This creates additional security considerations: + +### Risk + +A compromised Pulse container could expose SSH keys that access your Proxmox hosts, even with forced command restrictions. + +### Opt-In Required (v4.23.1+) + +Temperature monitoring now requires explicit confirmation during setup. The setup script displays a security notice and asks for your consent before enabling SSH access. + +### Runtime Warning + +Pulse logs a security warning at startup if it detects: +- Running in a container (Docker/LXC/containerd) +- SSH keys present for temperature monitoring + +This reminds you to review security hardening recommendations. + +### Hardening Recommendations + +#### 1. Key Rotation +Rotate SSH keys periodically (e.g., every 90 days): + +```bash +# On Pulse server +ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_new -N "" + +# Update all nodes' authorized_keys +# Test connectivity +ssh -i ~/.ssh/id_ed25519_new node "sensors -j" + +# Replace old key +mv ~/.ssh/id_ed25519_new ~/.ssh/id_ed25519 +``` + +#### 2. Secret Mounts (Docker) +Mount SSH keys from secure volumes: + +```yaml +version: '3' +services: + pulse: + image: rcourtman/pulse:latest + volumes: + - pulse-ssh-keys:/home/pulse/.ssh:ro # Read-only + - pulse-data:/data +volumes: + pulse-ssh-keys: + driver: local + driver_opts: + type: tmpfs # Memory-only, not persisted + device: tmpfs +``` + +#### 3. Monitoring & Alerts +Enable SSH audit logging on Proxmox nodes: + +```bash +# Install auditd +apt-get install auditd + +# Watch SSH access +auditctl -w /root/.ssh -p wa -k ssh_access + +# Monitor for unexpected commands +tail -f /var/log/audit/audit.log | grep ssh +``` + +#### 4. IP Restrictions +Limit SSH access to your Pulse server IP in `/etc/ssh/sshd_config`: + +```ssh +Match User root Address 192.168.1.100 + ForceCommand sensors -j + PermitOpen none + AllowAgentForwarding no + AllowTcpForwarding no +``` + +### Future: Agent-Based Architecture + +**Status:** Planned for future release + +The current SSH approach is a legacy implementation. Future versions will use agent-based monitoring where: + +- Lightweight temperature agents run on each Proxmox node +- Agents **push** metrics to Pulse over authenticated HTTPS +- No SSH keys stored in Pulse +- Better security boundary between monitoring and infrastructure + +This is the recommended architecture for production deployments. The SSH method will be maintained as a fallback for simple setups. + +### When to Use SSH vs Waiting for Agents + +**SSH Method (Current) - Acceptable for:** +- Home labs and trusted networks +- Non-containerized Pulse deployments +- Environments where you trust the container host + +**Wait for Agents - Better for:** +- Production infrastructure +- Multi-tenant environments +- High-security requirements +- Containerized Pulse with untrusted container hosts + +### Disabling Temperature Monitoring + +To remove SSH access: + +```bash +# On each Proxmox node +sed -i '/pulse@/d' /root/.ssh/authorized_keys + +# Or remove just the forced command entry +sed -i '/command="sensors -j"/d' /root/.ssh/authorized_keys +``` + +Temperature data will stop appearing in the dashboard after the next polling cycle. diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index e839a2706..f74c400f8 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -3211,6 +3211,54 @@ echo "Temperature Monitoring Setup (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" +# Check if Pulse is running in a container +PULSE_IS_CONTAINERIZED=false +if grep -q "pulse.lan\|pulse.home\|192.168" <<< "%s"; then + # Try to detect if target Pulse is containerized + # This is a heuristic - we can't know for sure from the setup script + : +fi + +echo "⚠️ SECURITY NOTICE" +echo "" +echo "Temperature monitoring requires SSH access from Pulse to this node." +echo "This creates the following security considerations:" +echo "" +echo " • SSH private keys will be stored on the Pulse server" +echo " • If Pulse runs in a container (LXC/Docker), keys live inside it" +echo " • Compromised Pulse = potential access to Proxmox hosts" +echo "" +echo "Mitigations in place:" +echo " • Forced command restriction (only 'sensors -j' can run)" +echo " • No port forwarding, X11, or PTY allocation" +echo "" +echo "This is a legacy feature. Future versions will use agent-based" +echo "architecture where nodes push metrics to Pulse (more secure)." +echo "" +echo "Do you want to enable temperature monitoring? [y/N]" +echo -n "> " + +ENABLE_TEMP_MONITORING="n" +if [ -t 0 ]; then + read -n 1 -r ENABLE_TEMP_MONITORING +else + if read -n 1 -r ENABLE_TEMP_MONITORING /dev/null; then + : + else + echo "(No terminal available - skipping temperature monitoring)" + ENABLE_TEMP_MONITORING="n" + fi +fi +echo "" +echo "" + +if [[ ! $ENABLE_TEMP_MONITORING =~ ^[Yy]$ ]]; then + echo "Temperature monitoring skipped." + echo "" + # Jump to the end of temperature setup section + SSH_PUBLIC_KEY="" +else + # SSH public key embedded from Pulse server SSH_PUBLIC_KEY="%s" SSH_RESTRICTED_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_PUBLIC_KEY" @@ -3506,6 +3554,7 @@ EOF fi fi fi +fi # End of ENABLE_TEMP_MONITORING check echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -3532,7 +3581,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then fi `, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP, tokenName, tokenName, tokenName, tokenName, tokenName, tokenName, - authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshPublicKey, pulseURL, authToken, tokenName, serverHost) + authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, pulseURL, sshPublicKey, sshPublicKey, pulseURL, authToken, tokenName, serverHost) } else { // PBS script = fmt.Sprintf(`#!/bin/bash diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index b43c5451b..6e37b56e4 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -7,6 +7,7 @@ import ( "math" "net" "net/url" + "os" "sort" "strconv" "strings" @@ -808,12 +809,64 @@ func (m *Monitor) GetConnectionStatuses() map[string]bool { return statuses } +// checkContainerizedTempMonitoring logs a security warning if Pulse is running +// in a container with SSH-based temperature monitoring enabled +func checkContainerizedTempMonitoring() { + // Check if running in container + isContainer := os.Getenv("PULSE_DOCKER") == "true" || isRunningInContainer() + if !isContainer { + return + } + + // Check if SSH keys exist (indicates temperature monitoring is configured) + homeDir := os.Getenv("HOME") + if homeDir == "" { + homeDir = "/home/pulse" + } + sshKeyPath := homeDir + "/.ssh/id_ed25519" + if _, err := os.Stat(sshKeyPath); err != nil { + // No SSH key found, temperature monitoring not configured + return + } + + // Log warning + log.Warn(). + Msg("🔐 SECURITY NOTICE: Pulse is running in a container with SSH-based temperature monitoring enabled. " + + "SSH private keys are stored inside the container, which could be a security risk if the container is compromised. " + + "Future versions will use agent-based architecture for better security. " + + "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 default SSH settings // Will use root user for now - can be made configurable later tempCollector := NewTemperatureCollector("root", "") + // Security warning if running in container with SSH temperature monitoring + checkContainerizedTempMonitoring() + m := &Monitor{ config: cfg, state: models.NewState(),