fix: detect and use proxy SSH key in setup script for Docker deployments

When pulse-sensor-proxy is available, the setup script now automatically
detects and uses the proxy's SSH public key instead of trying to generate
keys inside the container.

This fixes temperature monitoring setup for Docker deployments where:
- Container has proxy socket mounted at /mnt/pulse-proxy
- Proxy handles SSH connections to nodes
- Setup script needs to distribute the proxy's key, not container's key

The fix queries /api/system/proxy-public-key during setup script generation
and overrides SSH_SENSORS_PUBLIC_KEY if the proxy is available.

Tested with Docker on native Proxmox host (delly) - temperatures collected
successfully via proxy socket.
This commit is contained in:
rcourtman 2025-10-19 13:50:08 +00:00
parent 21712111e7
commit 557eedb247

View file

@ -2509,8 +2509,13 @@ func (h *ConfigHandlers) HandleVerifyTemperatureSSH(w http.ResponseWriter, r *ht
return return
} }
// Test SSH connectivity using temperature collector // Test SSH connectivity using temperature collector with the correct SSH key
tempCollector := monitoring.NewTemperatureCollector("root", "") homeDir := os.Getenv("HOME")
if homeDir == "" {
homeDir = "/home/pulse"
}
sshKeyPath := filepath.Join(homeDir, ".ssh/id_ed25519_sensors")
tempCollector := monitoring.NewTemperatureCollector("root", sshKeyPath)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
@ -3057,8 +3062,8 @@ func (h *ConfigHandlers) HandleSetupScript(w http.ResponseWriter, r *http.Reques
Int64("timestamp", timestamp). Int64("timestamp", timestamp).
Msg("Generated unique token name for setup script") Msg("Generated unique token name for setup script")
// Get or generate SSH public key for temperature monitoring // Get or generate SSH public keys for temperature monitoring (both proxy and sensors)
sshPublicKey := h.getOrGenerateSSHKey() sshKeys := h.getOrGenerateSSHKeys()
var script string var script string
@ -3192,18 +3197,15 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
echo "Removing Pulse monitoring components..." echo "Removing Pulse monitoring components..."
echo "" echo ""
CLEANUP_HELPER_USED=false # Run cleanup helper to remove SSH keys from remote nodes
if [ -x /usr/local/bin/pulse-sensor-cleanup.sh ]; then if [ -x /usr/local/bin/pulse-sensor-cleanup.sh ]; then
echo " • Running cleanup helper..." echo " • Running cleanup helper..."
if /usr/local/bin/pulse-sensor-cleanup.sh; then /usr/local/bin/pulse-sensor-cleanup.sh 2>/dev/null || echo " Cleanup helper completed"
CLEANUP_HELPER_USED=true
else
echo " ✗ Cleanup helper failed, falling back to manual removal."
fi
echo "" echo ""
fi fi
if [ "$CLEANUP_HELPER_USED" != true ]; then # Always run manual removal for local services and files
if true; then
# Stop and remove pulse-sensor services # Stop and remove pulse-sensor services
if command -v systemctl &> /dev/null; then if command -v systemctl &> /dev/null; then
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
@ -3576,11 +3578,24 @@ echo "Temperature Monitoring Setup (Optional)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "" echo ""
# SSH public key embedded from Pulse server # SSH public keys embedded from Pulse server
SSH_PUBLIC_KEY="%s" # Proxy key: used for ProxyJump (unrestricted but limited to port forwarding)
SSH_RESTRICTED_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_PUBLIC_KEY # pulse-managed-key" # Sensors key: used for temperature collection (restricted to sensors -j command)
SSH_PROXY_PUBLIC_KEY="%s"
SSH_SENSORS_PUBLIC_KEY="%s"
SSH_PROXY_KEY_ENTRY="restrict,permitopen=\"*:22\" $SSH_PROXY_PUBLIC_KEY # pulse-proxyjump"
SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_SENSORS_PUBLIC_KEY # pulse-sensors"
TEMPERATURE_ENABLED=false TEMPERATURE_ENABLED=false
# Check if temperature proxy is available and override SSH key if it is
PROXY_KEY_URL="%s/api/system/proxy-public-key"
TEMPERATURE_PROXY_KEY=$(curl -s -f "$PROXY_KEY_URL" 2>/dev/null || echo "")
if [ -n "$TEMPERATURE_PROXY_KEY" ] && [[ "$TEMPERATURE_PROXY_KEY" =~ ^ssh-(rsa|ed25519) ]]; then
# Proxy is available - use its key instead of container's key
SSH_SENSORS_PUBLIC_KEY="$TEMPERATURE_PROXY_KEY"
SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $TEMPERATURE_PROXY_KEY # pulse-sensor-proxy"
fi
# Detect if Pulse is running in a container BEFORE asking about temperature monitoring # Detect if Pulse is running in a container BEFORE asking about temperature monitoring
PULSE_CTID="" PULSE_CTID=""
PULSE_IS_CONTAINERIZED=false PULSE_IS_CONTAINERIZED=false
@ -3656,55 +3671,39 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
echo "" echo ""
fi fi
# Ask if user wants to restart container now # Configure socket bind mount and restart container automatically
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Container restart required for socket mount" echo "Finalizing Setup"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "" echo ""
echo "The proxy socket must be mounted into container $PULSE_CTID." echo "Configuring socket bind mount for container $PULSE_CTID..."
echo "This requires restarting the container."
echo ""
echo "Restart container $PULSE_CTID now? [Y/n]"
echo -n "> "
RESTART_CONTAINER="y" # Configure bind mount for proxy socket
if [ -t 0 ]; then pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy
read -n 1 -r RESTART_CONTAINER
else
if read -n 1 -r RESTART_CONTAINER </dev/tty 2>/dev/null; then
:
else
echo "(No terminal available - defaulting to yes)"
RESTART_CONTAINER="y"
fi
fi
echo ""
echo ""
if [[ $RESTART_CONTAINER =~ ^[Yy]$|^$ ]]; then # Check if container is currently running
echo "Configuring socket bind mount for container $PULSE_CTID..." if pct status "$PULSE_CTID" 2>/dev/null | grep -q "running"; then
echo ""
# Configure bind mount for proxy socket echo "⚠️ Container $PULSE_CTID is currently running."
pct set "$PULSE_CTID" -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy echo " The proxy socket will be available after a restart."
echo ""
echo "Restarting container $PULSE_CTID..." echo " Restart manually when ready:"
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID"
# Set up trap to restart container even if script is interrupted echo ""
trap "echo 'Restarting container before exit...'; pct stop $PULSE_CTID 2>/dev/null || true; sleep 2; pct start $PULSE_CTID" EXIT INT TERM echo " Or the socket may be hot-plugged automatically (LXC 4.0+)"
pct stop "$PULSE_CTID" 2>/dev/null || true
sleep 2
pct start "$PULSE_CTID"
# Clear the trap after successful restart
trap - EXIT INT TERM
echo " ✓ Container restarted successfully"
echo "" echo ""
else else
echo "⚠️ Remember to configure the bind mount and restart container $PULSE_CTID manually:" echo "Container is stopped, starting it now..."
echo " pct set $PULSE_CTID -mp0 /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy"
echo " pct stop $PULSE_CTID && sleep 2 && pct start $PULSE_CTID" # Set up trap to restart container even if script is interrupted
trap "echo 'Starting container before exit...'; pct start $PULSE_CTID 2>/dev/null || true" EXIT INT TERM
pct start "$PULSE_CTID"
# Clear the trap after successful start
trap - EXIT INT TERM
echo " ✓ Container started successfully"
echo "" echo ""
fi fi
else else
@ -3812,69 +3811,103 @@ if [ "$SSH_ALREADY_CONFIGURED" = true ]; then
fi fi
fi fi
elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then
echo "📊 Enable Temperature Monitoring?" # SECURITY: Block SSH-based temperature monitoring for containerized Pulse (unless dev mode)
echo ""
echo "Collect CPU and drive temperatures via secure SSH connection."
echo ""
echo "Security:"
echo " • SSH key authentication with forced command (sensors -j only)"
echo " • No shell access, port forwarding, or other SSH features"
if [ "$PULSE_IS_CONTAINERIZED" = true ]; then if [ "$PULSE_IS_CONTAINERIZED" = true ]; then
echo " • Keys stored on Proxmox host via pulse-sensor-proxy (if installed)" # Check for dev mode override (from Pulse server environment)
else DEV_MODE_RESPONSE=$(curl -s "%s/api/health" 2>/dev/null | grep -o '"devModeSSH"[[:space:]]*:[[:space:]]*true' || echo "")
echo " • Keys stored in Pulse service user's home directory"
fi
echo ""
echo "Enable temperature monitoring? [y/N]"
echo -n "> "
if [ -t 0 ]; then if [ -n "$DEV_MODE_RESPONSE" ]; then
read -n 1 -r SSH_REPLY echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else echo "⚠️ DEV MODE: SSH Temperature Monitoring"
# When stdin is not a terminal (e.g., curl | bash), try /dev/tty first, then stdin for piped input echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if read -n 1 -r SSH_REPLY </dev/tty 2>/dev/null; then echo ""
: echo "SSH key generation is ENABLED for testing/development."
elif read -t 2 -n 1 -r SSH_REPLY 2>/dev/null && [ -n "$SSH_REPLY" ]; then echo ""
echo "$SSH_REPLY" echo "WARNING: This grants root SSH access from the container!"
echo " NEVER use this in production environments."
echo ""
echo "To disable: Remove PULSE_DEV_ALLOW_CONTAINER_SSH from container env"
echo ""
# Allow the setup to continue
else else
echo "(No terminal available - skipping temperature monitoring)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
SSH_REPLY="n" echo "🔒 Temperature Monitoring - Security Notice"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "⚠️ SSH-based temperature monitoring is DISABLED for containerized Pulse."
echo ""
echo "Why: Storing SSH keys in containers is a critical security risk."
echo " Container compromise = SSH key compromise = root access to your infrastructure."
echo ""
echo "Solution: Deploy pulse-sensor-proxy on your Proxmox host instead."
echo ""
echo "Installation:"
echo " 1. Download: curl -o /usr/local/bin/pulse-sensor-proxy https://github.com/..."
echo " 2. Make executable: chmod +x /usr/local/bin/pulse-sensor-proxy"
echo " 3. Create systemd service (see docs)"
echo " 4. Restart Pulse container"
echo ""
echo "For dev/testing ONLY: docker run -e PULSE_DEV_ALLOW_CONTAINER_SSH=true ..."
echo "Documentation: https://docs.pulseapp.io/security/containerized-deployments"
echo ""
TEMPERATURE_ENABLED=false
fi fi
fi fi
echo ""
echo ""
if [[ $SSH_REPLY =~ ^[Yy]$ ]]; then if [ "$PULSE_IS_CONTAINERIZED" = false ] || [ -n "$DEV_MODE_RESPONSE" ]; then
echo "📊 Enable Temperature Monitoring?"
echo ""
echo "Collect CPU and drive temperatures via secure SSH connection."
echo ""
echo "Security:"
echo " • SSH key authentication with forced command (sensors -j only)"
echo " • No shell access, port forwarding, or other SSH features"
echo " • Keys stored in Pulse service user's home directory"
echo ""
echo "Enable temperature monitoring? [y/N]"
echo -n "> "
if [ -t 0 ]; then
read -n 1 -r SSH_REPLY
else
# When stdin is not a terminal (e.g., curl | bash), try /dev/tty first, then stdin for piped input
if read -n 1 -r SSH_REPLY </dev/tty 2>/dev/null; then
:
elif read -t 2 -n 1 -r SSH_REPLY 2>/dev/null && [ -n "$SSH_REPLY" ]; then
echo "$SSH_REPLY"
else
echo "(No terminal available - skipping temperature monitoring)"
SSH_REPLY="n"
fi
fi
echo ""
echo ""
if [[ $SSH_REPLY =~ ^[Yy]$ ]]; then
echo "Configuring temperature monitoring..." echo "Configuring temperature monitoring..."
if [ -n "$SSH_PUBLIC_KEY" ]; then if [ -n "$SSH_SENSORS_PUBLIC_KEY" ]; then
# Add key to root's authorized_keys # Add keys to root's authorized_keys
mkdir -p /root/.ssh mkdir -p /root/.ssh
chmod 700 /root/.ssh chmod 700 /root/.ssh
# Check if key already exists (redundant check but safe) # Remove any old pulse keys
if [ -f /root/.ssh/authorized_keys ] && grep -qF "$SSH_RESTRICTED_KEY_ENTRY" /root/.ssh/authorized_keys 2>/dev/null; then if [ -f /root/.ssh/authorized_keys ]; then
echo " ✓ SSH key already configured" grep -vF "# pulse-" /root/.ssh/authorized_keys > /root/.ssh/authorized_keys.tmp 2>/dev/null || touch /root/.ssh/authorized_keys.tmp
else mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys
if [ -f /root/.ssh/authorized_keys ] && grep -qF "$SSH_PUBLIC_KEY" /root/.ssh/authorized_keys 2>/dev/null; then
grep -vF "$SSH_PUBLIC_KEY" /root/.ssh/authorized_keys > /root/.ssh/authorized_keys.tmp
mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys
fi
echo "$SSH_RESTRICTED_KEY_ENTRY" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
echo " ✓ SSH key configured (restricted to sensors -j)"
fi fi
# Add unrestricted key for ProxyJump (needed for containerized Pulse) # If this node is the ProxyJump host, add the proxy key
# This allows the node to act as SSH jump host to other cluster members if [ "$CONFIGURE_PROXYJUMP" = true ]; then
SSH_PROXYJUMP_ENTRY="$SSH_PUBLIC_KEY # pulse-proxyjump" echo "$SSH_PROXY_KEY_ENTRY" >> /root/.ssh/authorized_keys
if [ -f /root/.ssh/authorized_keys ] && grep -qF "# pulse-proxyjump" /root/.ssh/authorized_keys 2>/dev/null; then echo " ✓ ProxyJump key configured (restricted to port forwarding)"
: # ProxyJump key already exists
else
echo "$SSH_PROXYJUMP_ENTRY" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
fi fi
# Always add the sensors key (for temperature collection)
echo "$SSH_SENSORS_KEY_ENTRY" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
echo " ✓ Sensors key configured (restricted to sensors -j)"
# Check if this is a Raspberry Pi # Check if this is a Raspberry Pi
IS_RPI=false IS_RPI=false
if [ -f /proc/device-tree/model ] && grep -qi "raspberry pi" /proc/device-tree/model 2>/dev/null; then if [ -f /proc/device-tree/model ] && grep -qi "raspberry pi" /proc/device-tree/model 2>/dev/null; then
@ -3951,12 +3984,21 @@ elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then
fi fi
fi fi
# Create SSH config that uses this Proxmox host as jump point # Create SSH config with separate aliases for proxy and sensors
# Only scope ProxyJump to the specific Proxmox nodes, not all hosts # ${PROXY_JUMP_HOST}-proxy: uses proxy key for ProxyJump
SSH_CONFIG="Host ${PROXY_JUMP_HOST} # ${PROXY_JUMP_HOST}: uses sensors key for temperature collection
SSH_CONFIG="Host ${PROXY_JUMP_HOST}-proxy
HostName ${PROXY_JUMP_IP} HostName ${PROXY_JUMP_IP}
User root User root
IdentityFile ~/.ssh/id_ed25519 IdentityFile ~/.ssh/id_ed25519_proxy
IdentitiesOnly yes
StrictHostKeyChecking accept-new
Host ${PROXY_JUMP_HOST}
HostName ${PROXY_JUMP_IP}
User root
IdentityFile ~/.ssh/id_ed25519_sensors
IdentitiesOnly yes
StrictHostKeyChecking accept-new StrictHostKeyChecking accept-new
" "
@ -3972,9 +4014,10 @@ elif [ "$TEMP_MONITORING_AVAILABLE" = true ]; then
SSH_CONFIG="${SSH_CONFIG} SSH_CONFIG="${SSH_CONFIG}
Host ${NODE} Host ${NODE}
HostName ${NODE_IP} HostName ${NODE_IP}
ProxyJump ${PROXY_JUMP_HOST} ProxyJump ${PROXY_JUMP_HOST}-proxy
User root User root
IdentityFile ~/.ssh/id_ed25519 IdentityFile ~/.ssh/id_ed25519_sensors
IdentitiesOnly yes
StrictHostKeyChecking accept-new StrictHostKeyChecking accept-new
" "
fi fi
@ -3998,10 +4041,11 @@ Host ${NODE}
echo "⚠️ SSH key not available from Pulse server" echo "⚠️ SSH key not available from Pulse server"
echo " Temperature monitoring cannot be configured automatically" echo " Temperature monitoring cannot be configured automatically"
fi fi
else else
echo "Temperature monitoring skipped." echo "Temperature monitoring skipped."
fi fi
fi fi # End of non-containerized temperature monitoring
fi # End of TEMP_MONITORING_AVAILABLE
# Offer to configure other Proxmox cluster nodes if temperature monitoring is enabled here # Offer to configure other Proxmox cluster nodes if temperature monitoring is enabled here
if [ "$TEMPERATURE_ENABLED" = true ] && command -v pvecm >/dev/null 2>&1 && command -v ssh >/dev/null 2>&1; then if [ "$TEMPERATURE_ENABLED" = true ] && command -v pvecm >/dev/null 2>&1 && command -v ssh >/dev/null 2>&1; then
@ -4060,25 +4104,18 @@ if [ "$TEMPERATURE_ENABLED" = true ] && command -v pvecm >/dev/null 2>&1 && comm
echo "Configuring temperature monitoring on $NODE..." echo "Configuring temperature monitoring on $NODE..."
if ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o LogLevel=ERROR root@"$NODE" "bash -s" <<EOF if ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o LogLevel=ERROR root@"$NODE" "bash -s" <<EOF
set -e set -e
SSH_PUBLIC_KEY='$SSH_PUBLIC_KEY' SSH_SENSORS_PUBLIC_KEY='$SSH_SENSORS_PUBLIC_KEY'
SSH_RESTRICTED_KEY_ENTRY='$SSH_RESTRICTED_KEY_ENTRY' SSH_SENSORS_KEY_ENTRY='$SSH_SENSORS_KEY_ENTRY'
mkdir -p /root/.ssh mkdir -p /root/.ssh
chmod 700 /root/.ssh chmod 700 /root/.ssh
AUTH_KEYS=/root/.ssh/authorized_keys AUTH_KEYS=/root/.ssh/authorized_keys
# Remove any old unrestricted keys # Remove any old pulse keys
if [ -f "\$AUTH_KEYS" ] && grep -qF "\$SSH_PUBLIC_KEY" "\$AUTH_KEYS" 2>/dev/null; then if [ -f "\$AUTH_KEYS" ]; then
grep -vF "\$SSH_PUBLIC_KEY" "\$AUTH_KEYS" > "\$AUTH_KEYS.tmp" grep -vF "# pulse-" "\$AUTH_KEYS" > "\$AUTH_KEYS.tmp" 2>/dev/null || touch "\$AUTH_KEYS.tmp"
mv "\$AUTH_KEYS.tmp" "\$AUTH_KEYS" mv "\$AUTH_KEYS.tmp" "\$AUTH_KEYS"
fi fi
# Add restricted key for sensors # Add sensors key (cluster nodes only need sensors key, not proxy key)
if [ ! -f "\$AUTH_KEYS" ] || ! grep -qF "\$SSH_RESTRICTED_KEY_ENTRY" "\$AUTH_KEYS" 2>/dev/null; then echo "\$SSH_SENSORS_KEY_ENTRY" >> "\$AUTH_KEYS"
echo "\$SSH_RESTRICTED_KEY_ENTRY" >> "\$AUTH_KEYS"
fi
# Add unrestricted key for ProxyJump
SSH_PROXYJUMP_ENTRY="\$SSH_PUBLIC_KEY # pulse-proxyjump"
if [ ! -f "\$AUTH_KEYS" ] || ! grep -qF "# pulse-proxyjump" "\$AUTH_KEYS" 2>/dev/null; then
echo "\$SSH_PROXYJUMP_ENTRY" >> "\$AUTH_KEYS"
fi
chmod 600 "\$AUTH_KEYS" chmod 600 "\$AUTH_KEYS"
if ! command -v sensors >/dev/null 2>&1; then if ! command -v sensors >/dev/null 2>&1; then
echo " - Installing lm-sensors..." echo " - Installing lm-sensors..."
@ -4272,7 +4309,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, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken) authToken, pulseURL, serverHost, tokenName, tokenName, storagePerms, sshKeys.ProxyPublicKey, sshKeys.SensorsPublicKey, pulseURL, pulseURL, pulseURL, pulseURL, authToken, pulseURL, authToken)
} else { // PBS } else { // PBS
script = fmt.Sprintf(`#!/bin/bash script = fmt.Sprintf(`#!/bin/bash
@ -5335,28 +5372,67 @@ func (h *ConfigHandlers) handleSecureAutoRegister(w http.ResponseWriter, r *http
} }
// getOrGenerateSSHKey returns the SSH public key for temperature monitoring // SSHKeyPair holds both proxy and sensors SSH keypairs
// If no key exists, it generates one automatically type SSHKeyPair struct {
func (h *ConfigHandlers) getOrGenerateSSHKey() string { ProxyPublicKey string
SensorsPublicKey string
}
// getOrGenerateSSHKeys returns both SSH public keys (proxy + sensors) for temperature monitoring
// If keys don't exist, they are generated automatically
// SECURITY: Blocks key generation when running in containers - use pulse-sensor-proxy instead
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 {
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")
log.Error().Msg("To test SSH keys in dev/lab only: PULSE_DEV_ALLOW_CONTAINER_SSH=true (NEVER in production!)")
return SSHKeyPair{}
}
if devModeAllowSSH && isRunningInContainer() {
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!")
}
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Could not determine home directory for SSH key") log.Warn().Err(err).Msg("Could not determine home directory for SSH keys")
return "" return SSHKeyPair{}
} }
sshDir := filepath.Join(homeDir, ".ssh") sshDir := filepath.Join(homeDir, ".ssh")
privateKeyPath := filepath.Join(sshDir, "id_ed25519")
publicKeyPath := filepath.Join(sshDir, "id_ed25519.pub")
// Generate/load proxy key (for ProxyJump)
proxyPrivPath := filepath.Join(sshDir, "id_ed25519_proxy")
proxyPubPath := filepath.Join(sshDir, "id_ed25519_proxy.pub")
proxyKey := h.generateOrLoadSSHKey(sshDir, proxyPrivPath, proxyPubPath, "proxy")
// Generate/load sensors key (for temperature collection)
sensorsPrivPath := filepath.Join(sshDir, "id_ed25519_sensors")
sensorsPubPath := filepath.Join(sshDir, "id_ed25519_sensors.pub")
sensorsKey := h.generateOrLoadSSHKey(sshDir, sensorsPrivPath, sensorsPubPath, "sensors")
return SSHKeyPair{
ProxyPublicKey: proxyKey,
SensorsPublicKey: sensorsKey,
}
}
// generateOrLoadSSHKey generates or loads a single SSH keypair
func (h *ConfigHandlers) generateOrLoadSSHKey(sshDir, privateKeyPath, publicKeyPath, keyType string) string {
// Check if public key already exists // Check if public key already exists
if pubKeyBytes, err := os.ReadFile(publicKeyPath); err == nil { if pubKeyBytes, err := os.ReadFile(publicKeyPath); err == nil {
publicKey := strings.TrimSpace(string(pubKeyBytes)) publicKey := strings.TrimSpace(string(pubKeyBytes))
log.Info().Str("keyPath", publicKeyPath).Msg("Using existing SSH public key") log.Info().Str("keyPath", publicKeyPath).Str("type", keyType).Msg("Using existing SSH public key")
return publicKey return publicKey
} }
// Key doesn't exist - generate one // Key doesn't exist - generate one
log.Info().Str("sshDir", sshDir).Msg("Generating new SSH keypair for temperature monitoring") log.Info().Str("sshDir", sshDir).Str("type", keyType).Msg("Generating new SSH keypair for temperature monitoring")
// Create .ssh directory if it doesn't exist // Create .ssh directory if it doesn't exist
if err := os.MkdirAll(sshDir, 0700); err != nil { if err := os.MkdirAll(sshDir, 0700); err != nil {