Pulse/SECURITY.md
rcourtman 0ca6001bad docs: update documentation after sensor proxy deprecation
Update docs to reflect the simplified temperature monitoring architecture:
- Remove references to pulse-sensor-proxy throughout
- Update TEMPERATURE_MONITORING.md to focus on unified agent approach
- Update CONFIGURATION.md, DEPLOYMENT_MODELS.md, FAQ.md
- Remove SECURITY_CHANGELOG.md (proxy-specific security notes)
- Clarify current recommended setup in various guides
2026-01-21 12:00:59 +00:00

591 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Pulse Security
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`.
For a high-level overview of the system design and data flow, please refer to
[`ARCHITECTURE.md`](ARCHITECTURE.md).
---
## Critical Security Notice for Container Deployments
### Container SSH Key Policy (BREAKING CHANGE)
**Effective immediately, SSH-based temperature monitoring is blocked in
containerized Pulse deployments.**
#### Why This Change?
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.
#### Affected Deployments
**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.
#### Migration Path (Production)
Preferred option (no SSH keys, no proxy wiring):
1. Install the unified agent (`pulse-agent`) on each Proxmox host with Proxmox integration enabled.
- Use the UI to generate an install command in **Settings → Agents → Installation commands**, or run:
```bash
curl -fsSL http://pulse.example.com:7655/install.sh | \
sudo bash -s -- --url http://pulse.example.com:7655 --token <api-token> --enable-proxmox
```
Legacy sensor proxy (removed):
- `pulse-sensor-proxy` is no longer supported. Migrate to `pulse-agent --enable-proxmox` or SSH-based collection.
- Cleanup steps are in `docs/TEMPERATURE_MONITORING.md`.
#### Removing Old SSH Keys
If you previously generated SSH keys inside containers:
```bash
# 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*
```
#### Security Boundary
```text
┌─────────────────────────────────────┐
│ Proxmox Host │
│ ┌───────────────────────────────┐ │
│ │ pulse-agent │ │
│ │ · Reads sensors locally │ │
│ │ · Sends metrics via HTTPS │ │
│ └───────────────────────────────┘ │
│ │ │
│ │ HTTPS + API token │
│ │ │
│ ┌─────────▼─────────────────────┐ │
│ │ Pulse (Docker/LXC container) │ │
│ │ · No SSH keys │ │
│ │ · No host root privileges │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
```
#### Homelab Exception
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.
#### Auditing Your Deployment
```bash
# Detect vulnerable containers
ls /home/pulse/.ssh/id_ed25519* 2>/dev/null && echo "⚠️ SSH keys present"
```
Verify temperature collection is agent-based:
- UI: **Settings → Agents** shows each Proxmox host connected and reporting.
- On each Proxmox host:
```bash
systemctl status pulse-agent
journalctl -u pulse-agent -n 200 --no-pager
```
**Documentation:** <https://github.com/rcourtman/Pulse/blob/main/SECURITY.md#critical-security-notice-for-container-deployments>
**Issues:** <https://github.com/rcourtman/pulse/issues>
**Private disclosures:** <security@pulseapp.io>
---
## Mandatory Authentication
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 registers `pulse-backend.service`,
> substitute that name in the commands below.
### First-Run Security Setup
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**
## Smart Security Context
### Public Access Detection
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
### Trusted Networks Configuration (Deprecated)
**Note:** authentication is now mandatory regardless of network location.
Legacy configuration (no longer applicable):
```bash
# 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 still requires authentication (no bypass).
- The trusted list only influences security posture warnings and diagnostics.
## Security Warning System
Pulse includes a non-intrusive security warning system that helps you
understand your security posture.
### Security Score
Your instance receives a score from 05 based on:
- ✅ Credentials encrypted at rest (always enabled)
- ✅ Export/import protection
- ⚠️ Authentication enabled
- ⚠️ HTTPS connection
- ⚠️ Audit logging
### Dismissing Warnings
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
## Credential Security
### Encrypted at Rest (AES-256-GCM)
- **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`)
- **Encryption key**: auto-generated (`/etc/pulse/.encryption.key`)
### Security Features
- **Logs**: token values masked with `***` in all outputs
- **API**: frontend receives only `hasToken: true`, never actual values
- **Export**: requires authentication (session, proxy auth, or `X-API-Token`
header) to extract credentials
- **Migration**: use passphrase-protected export/import (see
[Migration Guide](docs/MIGRATION.md))
- **Auto-migration**: unencrypted configs automatically migrate to encrypted
format
## Export/Import Protection
By default, configuration export/import is blocked. You have two options:
### Option 1: Create an API Token (Recommended)
Create a token in **Settings → API Tokens**, then use it for exports.
For automation-only environments, you can seed tokens via environment variables (legacy) and
they will be persisted to `api_tokens.json` on startup.
Legacy environment seeding:
```bash
# 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
```
### Option 2: Allow Unprotected Export (Homelab)
```bash
# 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.
## Security Features Summary
### Core Protection
- **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 → Overview*
### Advanced Security (When Authentication Enabled)
- **Password security**
- Bcrypt hashing with cost factor 12 (60character hash)
- Passwords never stored in plain text
- Automatic hashing during security setup
- **Critical**: bcrypt hashes must be exactly 60 characters
- **API token security**
- 64character hex tokens (32 bytes entropy)
- SHA3-256 hashed before storage (64character hash)
- Raw token shown only once
- Tokens never stored in plain text
- Stored in `api_tokens.json` and managed via the UI
- API-only mode supported (no password auth required)
- **CSRF protection**: all state-changing operations require CSRF tokens
- **Rate limiting**
- Auth endpoints: 10 attempts/minute per IP
- Config changes: 30 requests/minute per IP
- Exports: 5 requests per 5 minutes per IP
- Recovery operations: 3 requests per 10 minutes per IP
- Update checks/actions: 60 requests/minute per IP
- WebSocket connects: 30 requests/minute per IP
- General API: 500 requests/minute per IP
- Public endpoints: 1000 requests/minute per IP
- 429 responses include rate limit headers:
- `X-RateLimit-Limit`: Maximum requests per window
- `X-RateLimit-Remaining`: Requests remaining in current window
- `X-RateLimit-Reset`: Window reset timestamp
- `Retry-After`: Seconds to wait before retrying (on 429 responses)
- **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 (30 days when "Remember me" is enabled)
- Session invalidation on password change
- **Security headers**
- Content-Security-Policy
- X-Frame-Options: `DENY` by default (adjusted when `allowEmbedding` is enabled in system settings)
- 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**
- Authentication events include IP addresses
- Rollback actions are logged with timestamps and metadata
- Scheduler health escalations recorded in audit trail
- Runtime logging configuration changes tracked
- Security status reflects whether persistent audit logging is active (Pulse Pro)
### What's Encrypted in Exports
- Node credentials (passwords, API tokens)
- PBS credentials
- Email settings passwords
- Webhook URLs and authentication headers
### What's **Not** Encrypted
- Node hostnames and IPs
- Threshold settings
- General configuration
- Alert rules and schedules
## Authentication Workflows
Pulse supports multiple authentication methods that can be used independently or
together.
> **Note**: `DISABLE_AUTH` is deprecated and no longer disables authentication. Remove it from your environment and restart if it's still present.
### Password Authentication
#### Quick Security Setup (Recommended)
1. Navigate to *Settings → Security → Authentication*.
2. Click **Setup**.
3. Enter username and password.
4. Save the generated API token (shown only once!).
5. 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)
- Applies credentials immediately and persists them for future restarts
#### Manual Setup (Advanced)
```bash
# Using systemd (plain text will be auto-hashed)
sudo systemctl edit pulse
# Add:
[Service]
Environment="PULSE_AUTH_USER=admin"
Environment="PULSE_AUTH_PASS=$2a$12$..." # Prefer bcrypt hash for production; plain text is auto-hashed.
# 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.
#### Features
- Web UI login required when authentication enabled
- Change/remove password from Settings → Security → Authentication
- 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
### API Token Authentication
For programmatic access and automation. API tokens are SHA3-256 hashed for security.
#### Token Setup via Quick 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
#### Manual Token Setup (Legacy Seeding)
```bash
# 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 in `api_tokens.json`. Plain values never persist beyond startup.
#### Token Management (Settings → API Tokens)
- 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
#### Usage
```bash
# Include the ORIGINAL token (not hash) in X-API-Token header
curl -H "X-API-Token: your-original-token" http://localhost:7655/api/health
# Export config requires auth + passphrase (min 12 chars)
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Token: your-original-token" \
-d '{"passphrase":"use-a-strong-passphrase"}' \
http://localhost:7655/api/config/export
```
Most API endpoints also accept `Authorization: Bearer <token>`, but export/import uses the `X-API-Token` header.
### Auto-Registration Security
#### Default Mode
- All access requires authentication
- Nodes can auto-register with the API token
- Setup scripts work without additional configuration
#### Secure Mode
- Require API token for all operations
- Protects auto-registration endpoint
- Enable by creating at least one API token (UI or legacy env seeding)
### Runtime Logging Configuration
Pulse supports configurable logging (level, format, optional file output, rotation) via environment variables.
#### Security Benefits
- 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
#### Configuration Options
**Via environment variables:**
```bash
# 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_AGE=30" # Days to retain logs
Environment="LOG_COMPRESS=true" # Compress rotated logs
# Docker
docker run \
-e LOG_LEVEL=info \
-e LOG_FORMAT=json \
-e LOG_MAX_SIZE=100 \
-e LOG_MAX_AGE=30 \
-e LOG_COMPRESS=true \
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 take effect on restart
## CORS (Cross-Origin Resource Sharing)
By default, Pulse does **not** enable CORS (same-origin only). Configure allowed origins only when
you need cross-origin access (for example, a separate UI domain or external tooling).
### Configuring CORS for External Access
If you need to access the Pulse API from a different domain, configure **Settings → System → Network**
or use environment overrides:
```bash
# 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"
# Development (allow localhost)
ALLOWED_ORIGINS="http://localhost:5173"
```
Notes:
- `ALLOWED_ORIGINS` supports a single origin or `*` (it is written directly to `Access-Control-Allow-Origin`).
- In production, set a specific origin to avoid exposing the API to arbitrary sites.
- For local dev, Pulse auto-allows `http://localhost:5173` and `http://localhost:7655` when `NODE_ENV=development` or `PULSE_DEV=true`.
## Monitoring and Observability
### Scheduler Health API
#### Endpoint
```bash
curl -s http://localhost:7655/api/monitoring/scheduler/health | jq
```
#### Security Use Cases
1. **Anomaly Detection**
- Watch for unusual queue depths (possible DoS)
- Monitor circuit breaker trips (connectivity issues or attacks)
- Track backoff patterns (rate limiting, potential probes)
2. **Performance Monitoring**
- Identify performance degradation
- Detect resource exhaustion
- Track API response times
3. **Incident Response**
- Real-time visibility into system health
- Historical metrics for post-incident analysis
- Circuit breaker status for failover decisions
#### Key Security Metrics
- **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
There is currently no dedicated scheduler-health UI in v5. Use the API endpoint above (or export diagnostics from **Settings → Diagnostics**) when troubleshooting.
## Security Best Practices
### Credential Storage
- ✅ **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
### Authentication Setup
- ✅ **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
### Verification Checklist
Manually verify your deployment follows security best practices:
- No hardcoded credentials in environment files
- No credentials exposed in logs (check `docker logs pulse`)
- All passwords stored as bcrypt hashes (60 characters, starting with `$2a$` or `$2b$`)
- All API tokens stored as SHA3-256 hashes (64 characters)
- Secure file permissions on `/etc/pulse/.env` (600)
- No credential leaks in API responses (test with `curl`)
## Account Lockout and Recovery
### Lockout Behavior
- 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
### Automatic Recovery
- Lockouts automatically expire after 15 minutes
- No action needed - just wait for the timer to expire
- Successful login clears all failed attempt counters
### Manual Recovery (Admin)
Administrators with API access can manually reset lockouts:
```bash
# 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"}'
```
## Troubleshooting
**Account locked?** Wait 15 minutes or contact admin for manual reset
**Export blocked?** You're on a public network login with password, create an API token, 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 → API Tokens* (use the original token, not the hash)
**CORS errors?** Configure Allowed Origins in the UI or set `ALLOWED_ORIGINS` for your domain
**Forgot password?** Remove `.env` and restart Pulse, then use the bootstrap token to set new credentials
---