This document is the canonical security policy for Pulse. It combines our
ongoing hardening guidance with the operational checklists that previously lived
in docs/SECURITY.md
.
Effective immediately, SSH-based temperature monitoring is blocked in containerized Pulse deployments.
Storing SSH private keys inside Docker/LXC containers creates an unacceptable risk in production environments:
- Container compromise = infrastructure compromise – if an attacker gains shell access to the Pulse container they obtain the SSH private keys used to reach your Proxmox hosts.
- Keys persist in images – private keys survive in image layers and can leak when images are pushed to registries or shared.
- No key rotation – long-lived keys inside containers are difficult to rotate safely.
- Violates least-privilege – monitoring containers should not hold credentials that grant host-level access to the infrastructure they observe.
✅ Not affected – Pulse installed directly on a VM or bare-metal host (no containers), or homelab environments where you explicitly accept the risk.
❌ Blocked – Pulse running in Docker containers, LXC containers, or any
environment where PULSE_DOCKER=true
//.dockerenv
is detected.
- Deploy
pulse-sensor-proxy
on each Proxmox hostcurl -o /usr/local/bin/pulse-sensor-proxy \ https://github.com/rcourtman/pulse/releases/latest/download/pulse-sensor-proxy chmod +x /usr/local/bin/pulse-sensor-proxy
- Create a systemd unit (
/etc/systemd/system/pulse-sensor-proxy.service
)[Unit] Description=Pulse Temperature Sensor Proxy After=network.target [Service] Type=simple User=root ExecStart=/usr/local/bin/pulse-sensor-proxy Restart=on-failure [Install] WantedBy=multi-user.target
- Enable and start the service
systemctl daemon-reload systemctl enable --now pulse-sensor-proxy
- Restart the Pulse container so it binds to the proxy socket. The container will automatically fall back to socket-based temperature polling.
If you previously generated SSH keys inside containers:
# On each Proxmox host
sed -i '/# pulse-/d' /root/.ssh/authorized_keys
# Inside the Pulse container (or rebuild the container)
docker exec pulse rm -rf /home/pulse/.ssh/id_ed25519*
┌─────────────────────────────────────┐
│ Proxmox Host │
│ ┌───────────────────────────────┐ │
│ │ pulse-sensor-proxy (root) │ │
│ │ · Runs sensors -j │ │
│ │ · Exposes Unix socket only │ │
│ └───────────────────────────────┘ │
│ │ │
│ │ /run/pulse-sensor-proxy.sock
│ │ │
│ ┌─────────▼─────────────────────┐ │
│ │ Pulse container (bind mount) │ │
│ │ · No SSH keys │ │
│ │ · No host root privileges │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
If you fully understand the risk and are not containerized (VM/bare-metal
install), the legacy SSH flow still works. Use a dedicated monitoring user,
restrict the key with command="sensors -j"
and from="<pulse-ip>"
, and
rotate keys regularly.
# Detect vulnerable containers
ls /home/pulse/.ssh/id_ed25519* 2>/dev/null && echo "⚠️ SSH keys present"
# Check container logs for proxy detection
docker logs pulse | grep -i "temperature proxy detected"
# Verify the host service
systemctl status pulse-sensor-proxy
Documentation: https://docs.pulseapp.io/security/containerized-deployments
Issues: https://github.com/rcourtman/pulse/issues
Private disclosures: security@pulseapp.io
Starting with v4.5.0, authentication setup is prompted for all new Pulse installations. This protects your Proxmox API credentials from unauthorized access.
Service name note: systemd deployments use
pulse.service
. If you're upgrading from an older install that still registerspulse-backend.service
, substitute that name in the commands below.
When you first access Pulse, you'll be guided through a mandatory security setup:
- Create your admin username and password
- Automatic API token generation for automation
- Settings are applied immediately without restart
- Your existing nodes and settings are preserved
Pulse automatically detects when it's being accessed from public networks:
- Private networks: local/RFC1918 addresses (192.168.x.x, 10.x.x.x, etc.)
- Public networks: any non-private IP address
- Stronger warnings: red alerts when accessed from public IPs without authentication
Note: authentication is now mandatory regardless of network location.
Legacy configuration (no longer applicable):
# Environment variable (comma-separated CIDR blocks)
PULSE_TRUSTED_NETWORKS=192.168.1.0/24,10.0.0.0/24
# Or in systemd
sudo systemctl edit pulse
[Service]
Environment="PULSE_TRUSTED_NETWORKS=192.168.1.0/24,10.0.0.0/24"
When configured:
- Access from trusted networks: no auth required
- Access from outside: authentication enforced
- Useful for: mixed home/remote access scenarios
Pulse includes a non-intrusive security warning system that helps you understand your security posture.
Your instance receives a score from 0‑5 based on:
- ✅ Credentials encrypted at rest (always enabled)
- ✅ Export/import protection
⚠️ Authentication enabled⚠️ HTTPS connection⚠️ Audit logging
If you're comfortable with your security setup, you can dismiss warnings:
- For 1 day – reminder tomorrow
- For 1 week – reminder next week
- Forever – won't show again
- Node credentials: passwords and API tokens (
/etc/pulse/nodes.enc
) - Email settings: SMTP passwords (
/etc/pulse/email.enc
) - Webhook data: URLs and auth headers (
/etc/pulse/webhooks.enc
) – v4.1.9+ - Encryption key: auto-generated (
/etc/pulse/.encryption.key
)
- Logs: token values masked with
***
in all outputs - API: frontend receives only
hasToken: true
, never actual values - Export: requires a valid API token (
X-API-Token
header ortoken
parameter) to extract credentials - Migration: use passphrase-protected export/import (see Migration Guide)
- Auto-migration: unencrypted configs automatically migrate to encrypted format
By default, configuration export/import is blocked. You have two options:
# Using systemd (secure)
sudo systemctl edit pulse
# Add:
[Service]
Environment="API_TOKENS=ansible-token,docker-agent-token"
Environment="API_TOKEN=legacy-token"
# Then restart:
sudo systemctl restart pulse
# Docker
docker run -e API_TOKENS=ansible-token,docker-agent-token rcourtman/pulse:latest
# Using systemd
sudo systemctl edit pulse
# Add:
[Service]
Environment="ALLOW_UNPROTECTED_EXPORT=true"
# Docker
docker run -e ALLOW_UNPROTECTED_EXPORT=true rcourtman/pulse:latest
Note: for production, prefer Docker secrets or systemd environment files for sensitive data.
- Encryption: credentials encrypted at rest (AES-256-GCM)
- Export protection: exports always encrypted with a passphrase
- Minimum passphrase: 12 characters required for exports
- Security tab: check status in Settings → Security
- Password security
- Bcrypt hashing with cost factor 12 (60‑character hash)
- Passwords never stored in plain text
- Automatic hashing during security setup
- Critical: bcrypt hashes must be exactly 60 characters
- API token security
- 64‑character hex tokens (32 bytes entropy)
- SHA3-256 hashed before storage (64‑character hash)
- Raw token shown only once
- Tokens never stored in plain text
- Live reloading when
.env
changes - API-only mode supported (no password auth required)
- CSRF protection: all state-changing operations require CSRF tokens
- Rate limiting (enhanced in v4.24.0)
- Auth endpoints: 10 attempts/minute per IP (returns
Retry-After
header) - General API: 500 requests/minute per IP
- Real-time endpoints exempt for functionality
- New in v4.24.0: All responses include rate limit headers:
X-RateLimit-Limit
: Maximum requests per windowX-RateLimit-Remaining
: Requests remaining in current windowRetry-After
: Seconds to wait before retrying (on 429 responses)
- Auth endpoints: 10 attempts/minute per IP (returns
- Account lockout
- Locks after 5 failed login attempts
- 15-minute automatic lockout duration
- Clear feedback showing remaining attempts
- Time remaining displayed when locked
- Manual reset available via API for admins
- Session management
- Secure HttpOnly cookies
- 24-hour session expiry
- Session invalidation on password change
- Security headers
- Content-Security-Policy
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- X-XSS-Protection: 1; mode=block
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy restricting sensitive APIs
- Audit logging (enhanced in v4.24.0)
- Authentication events include IP addresses
- New: Rollback actions are logged with timestamps and metadata
- New: Scheduler health escalations recorded in audit trail
- New: Runtime logging configuration changes tracked
- Node credentials (passwords, API tokens)
- PBS credentials
- Email settings passwords
- Webhook URLs and authentication headers (v4.1.9+)
- Node hostnames and IPs
- Threshold settings
- General configuration
- Alert rules and schedules
Pulse supports multiple authentication methods that can be used independently or together.
- Navigate to Settings → Security.
- Click Enable Security Now.
- Enter username and password.
- Save the generated API token (shown only once!).
- Security is enabled immediately (no restart needed).
This automatically:
- Generates a secure random password
- Hashes it with bcrypt (cost factor 12)
- Creates secure API token (SHA3-256 hashed, raw token shown once)
- For systemd: Configures systemd with hashed credentials
- For Docker: Saves to
/data/.env
with hashed credentials (properly quoted to prevent shell expansion) - Restarts service/container with authentication enabled
# Using systemd (password will be hashed automatically)
sudo systemctl edit pulse
# Add:
[Service]
Environment="PULSE_AUTH_USER=admin"
Environment="PULSE_AUTH_PASS=$2a$12$..." # Use bcrypt hash, not plain text!
# Docker (credentials persist in volume via .env file)
# IMPORTANT: Always quote bcrypt hashes to prevent shell expansion!
docker run -e PULSE_AUTH_USER=admin -e PULSE_AUTH_PASS='$2a$12$...' rcourtman/pulse:latest
# Or use Quick Security Setup and restart container
Important: Always use hashed passwords in configuration. Use the Quick Security Setup or generate bcrypt hashes manually.
- Web UI login required when authentication enabled
- Change/remove password from Settings → Security
- Passwords ALWAYS hashed with bcrypt (cost 12)
- Session-based authentication with secure HttpOnly cookies
- 24-hour session expiry
- CSRF protection for all state-changing operations
- Session invalidation on password change
For programmatic access and automation. API tokens are SHA3-256 hashed for security.
The Quick Security Setup automatically:
- Generates a cryptographically secure token
- Hashes it with SHA3-256
- Stores only the 64-character hash
- Adds the token to the managed token list
# Using systemd (plain text values are auto-hashed on startup)
sudo systemctl edit pulse
# Add:
[Service]
Environment="API_TOKENS=ansible-token,docker-agent-token"
# Docker
docker run -e API_TOKENS=ansible-token,docker-agent-token rcourtman/pulse:latest
# To provide pre-hashed tokens instead, list the SHA3-256 hashes
# Environment="API_TOKENS=83c8...,b1de..."
Security Note: Tokens defined via environment variables are hashed with SHA3-256 before being stored on disk. Plain values never persist beyond startup.
- Issue dedicated tokens for automation/agents without sharing a global credential
- View prefixes/suffixes and last-used timestamps for auditing
- Revoke tokens individually without downtime
- Regenerate tokens when rotating credentials (new value displayed once)
- All tokens stored as SHA3-256 hashes
# Include the ORIGINAL token (not hash) in X-API-Token header
curl -H "X-API-Token: your-original-token" http://localhost:7655/api/health
# Or in query parameter for export/import
curl "http://localhost:7655/api/export?token=your-original-token"
- All access requires authentication
- Nodes can auto-register with the API token
- Setup scripts work without additional configuration
- Require API token for all operations
- Protects auto-registration endpoint
- Enable by setting at least one API token via
API_TOKENS
(or legacyAPI_TOKEN
) environment variable
New in v4.24.0: Adjust logging settings dynamically without restarting Pulse.
- Enable debug logging temporarily for incident investigation
- Switch to JSON format for SIEM integration
- Adjust verbosity based on security posture
- Control file rotation to manage audit log retention
Via UI: Navigate to Settings → System → Logging:
- Log Level:
debug
,info
,warn
,error
- Log Format:
json
(for log aggregation),text
(human-readable) - File Rotation: size limits, retention policies
Via Environment Variables:
# Systemd
sudo systemctl edit pulse
[Service]
Environment="LOG_LEVEL=info"
Environment="LOG_FORMAT=json"
Environment="LOG_MAX_SIZE=100" # MB per log file
Environment="LOG_MAX_BACKUPS=10" # Number of rotated logs to keep
Environment="LOG_MAX_AGE=30" # Days to retain logs
# Docker
docker run \
-e LOG_LEVEL=info \
-e LOG_FORMAT=json \
-e LOG_MAX_SIZE=100 \
-e LOG_MAX_BACKUPS=10 \
-e LOG_MAX_AGE=30 \
rcourtman/pulse:latest
Security Considerations:
- Debug logs may contain sensitive data—enable only when needed
- JSON format recommended for security monitoring and SIEM
- Adjust retention based on compliance requirements
- Changes are logged to audit trail
By default, Pulse only allows same-origin requests (no CORS headers). This is the most secure configuration.
If you need to access Pulse API from a different domain:
# Docker
docker run -e ALLOWED_ORIGINS="https://app.example.com" rcourtman/pulse:latest
# systemd
sudo systemctl edit pulse
[Service]
Environment="ALLOWED_ORIGINS=https://app.example.com"
# Multiple origins (comma-separated)
ALLOWED_ORIGINS="https://app.example.com,https://dashboard.example.com"
# Development mode (allows localhost)
PULSE_DEV=true
Security Note: Never use ALLOWED_ORIGINS=*
in production as it allows any website to access your API.
New in v4.24.0: Monitor Pulse's internal health and detect anomalies using the scheduler health API.
curl -s http://localhost:7655/api/monitoring/scheduler/health | jq
-
Anomaly Detection
- Watch for unusual queue depths (possible DoS)
- Monitor circuit breaker trips (connectivity issues or attacks)
- Track backoff patterns (rate limiting, potential probes)
-
Performance Monitoring
- Identify performance degradation
- Detect resource exhaustion
- Track API response times
-
Incident Response
- Real-time visibility into system health
- Historical metrics for post-incident analysis
- Circuit breaker status for failover decisions
- Queue Depth: High values may indicate attack or overload
- Circuit Breaker Status: Half-open/open states suggest connectivity issues
- Backoff Delays: Increased backoff may indicate rate limiting or errors
- Error Rates: Track failed API calls and authentication attempts
Dashboard Access: Navigate to Settings → System → Monitoring for visual representation of scheduler health.
- ✅ DO: Use Quick Security Setup for automatic hashing
- ✅ DO: Store only bcrypt hashes for passwords
- ✅ DO: Store only SHA3-256 hashes for API tokens
- ❌ DON'T: Store plain text passwords in config files
- ❌ DON'T: Store plain text API tokens in config files
- ❌ DON'T: Log credentials or include them in backups
- ✅ DO: Use strong, unique passwords (16+ characters)
- ✅ DO: Rotate API tokens periodically
- ✅ DO: Use HTTPS in production environments
- ❌ DON'T: Share API tokens between users/services
- ❌ DON'T: Embed credentials in client-side code
Run the security verification script to ensure no plain text credentials:
/opt/pulse/testing-tools/security-verification.sh
This checks:
- No hardcoded credentials in code
- No credentials exposed in logs
- All passwords/tokens properly hashed
- Secure file permissions
- No credential leaks in API responses
- After 5 failed login attempts, the account is locked for 15 minutes
- Lockout applies to both username and IP address
- Login form shows remaining attempts after each failure
- Clear message when locked with time remaining
- Lockouts automatically expire after 15 minutes
- No action needed - just wait for the timer to expire
- Successful login clears all failed attempt counters
Administrators with API access can manually reset lockouts:
# Reset lockout for a specific username
curl -X POST http://localhost:7655/api/security/reset-lockout \
-H "X-API-Token: your-api-token" \
-H "Content-Type: application/json" \
-d '{"identifier":"username"}'
# Reset lockout for an IP address
curl -X POST http://localhost:7655/api/security/reset-lockout \
-H "X-API-Token: your-api-token" \
-H "Content-Type: application/json" \
-d '{"identifier":"192.168.1.100"}'
Account locked? Wait 15 minutes or contact admin for manual reset
Export blocked? You're on a public network – login with password, set an API token (API_TOKENS
), or set ALLOW_UNPROTECTED_EXPORT=true
Rate limited? Wait 1 minute and try again
Can't login? Check PULSE_AUTH_USER
and PULSE_AUTH_PASS
environment variables
API access denied? Verify the token you supplied matches one of the values created in Settings → Security → API tokens (use the original token, not the hash)
CORS errors? Configure ALLOWED_ORIGINS
for your domain
Forgot password? Start fresh – delete your Pulse data and restart
Last updated: 2025-10-20
Version 4.24.0 Security Enhancements:
- ✅ X-RateLimit-* headers for all API responses
- ✅ Runtime logging configuration for incident response
- ✅ Scheduler health API for anomaly detection
- ✅ Enhanced audit logging (rollback actions, scheduler events)
- ✅ Adaptive polling with circuit breakers and backoff
- ✅ Shared script library system (secure installer patterns)