fix: Add security gates for containerized temperature monitoring
Addresses #528 - Added opt-in confirmation prompt to setup script with security notice - Added runtime warning when containerized Pulse uses SSH temperature monitoring - Documented security considerations and hardening recommendations - Users must explicitly confirm understanding before enabling in containers
This commit is contained in:
parent
bebe5efc3d
commit
c8e3c93516
3 changed files with 226 additions and 1 deletions
|
|
@ -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)
|
- Timeout: 5 seconds (non-blocking)
|
||||||
- Falls back gracefully if SSH fails
|
- Falls back gracefully if SSH fails
|
||||||
- No impact if SSH is not configured
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -3211,6 +3211,54 @@ echo "Temperature Monitoring Setup (Optional)"
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
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/tty 2>/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 embedded from Pulse server
|
||||||
SSH_PUBLIC_KEY="%s"
|
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"
|
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
|
||||||
fi
|
fi
|
||||||
|
fi # End of ENABLE_TEMP_MONITORING check
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
@ -3532,7 +3581,7 @@ if [ "$AUTO_REG_SUCCESS" != true ]; then
|
||||||
fi
|
fi
|
||||||
`, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP,
|
`, serverName, time.Now().Format("2006-01-02 15:04:05"), pulseIP,
|
||||||
tokenName, tokenName, tokenName, tokenName, tokenName, tokenName,
|
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
|
} else { // PBS
|
||||||
script = fmt.Sprintf(`#!/bin/bash
|
script = fmt.Sprintf(`#!/bin/bash
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -808,12 +809,64 @@ func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||||
return statuses
|
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
|
// New creates a new Monitor instance
|
||||||
func New(cfg *config.Config) (*Monitor, error) {
|
func New(cfg *config.Config) (*Monitor, error) {
|
||||||
// Initialize temperature collector with default SSH settings
|
// Initialize temperature collector with default SSH settings
|
||||||
// Will use root user for now - can be made configurable later
|
// Will use root user for now - can be made configurable later
|
||||||
tempCollector := NewTemperatureCollector("root", "")
|
tempCollector := NewTemperatureCollector("root", "")
|
||||||
|
|
||||||
|
// Security warning if running in container with SSH temperature monitoring
|
||||||
|
checkContainerizedTempMonitoring()
|
||||||
|
|
||||||
m := &Monitor{
|
m := &Monitor{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
state: models.NewState(),
|
state: models.NewState(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue