v5: gate legacy sensor-proxy and prune dev docs

This commit is contained in:
rcourtman 2025-12-18 21:51:25 +00:00
parent 862310c4c6
commit 337987cb56
25 changed files with 149 additions and 1905 deletions

5
.gitignore vendored
View file

@ -139,6 +139,7 @@ mock.env.backup
# Sensitive files - DO NOT COMMIT
secrets.env
*secret*.env
docs/PULSE_PRO_IMPLEMENTATION.md
# Browser/session artifacts
**/cookies.txt
@ -149,6 +150,10 @@ secrets.env
# Development documentation (local only)
CLAUDE_DEV_SETUP.md
AGENT_METRICS_*.md
DEV-QUICK-START.md
docs/DOCS_AUDIT_*.md
docs/SECURITY_AUDIT_*.md
docs/development/
# Temporary scripts
tmp_*.py

View file

@ -43,7 +43,7 @@ cd ..
npm run mock:on # Optional: enable mock data
```
See `docs/development/MOCK_MODE.md` for tips on synthetic fixtures.
Mock mode is supported for development, but the internal developer notes are not shipped in this repository.
---

View file

@ -1,99 +0,0 @@
# Development Quick Start
## Prerequisites
- Go **1.24.9** or newer
- Node.js 20+
- pnpm 9+ (for frontend work)
> **Tip**: Read [`ARCHITECTURE.md`](ARCHITECTURE.md) to understand the system design before diving in.
## Hot-Reload Development Mode
Start the development environment with hot-reload:
```bash
./scripts/hot-dev.sh
```
This starts:
- Backend API on port 7656
- Frontend on port 7655 with hot-reload
- Both backend and frontend automatically reload on code changes
Access the app at: http://localhost:7655 or http://192.168.0.123:7655
## Toggle Between Mock and Production Data
Switch modes seamlessly without manually restarting services:
```bash
# Enable mock mode (test with fake data)
npm run mock:on
# Disable mock mode (use real Proxmox nodes)
npm run mock:off
# Check current mode
npm run mock:status
# Edit mock configuration
npm run mock:edit
```
Or use the script directly:
```bash
./scripts/toggle-mock.sh on # Enable mock mode
./scripts/toggle-mock.sh off # Disable mock mode (use production data)
./scripts/toggle-mock.sh status # Show current status
```
The toggle script automatically:
- Updates `mock.env` configuration
- Restarts the backend with new settings
- Keeps the frontend running (no restart needed)
- Syncs production config when switching to production mode
- Switches `PULSE_DATA_DIR` between `/opt/pulse/tmp/mock-data` (mock) and `/etc/pulse` (production) so test data never touches real credentials
## Mock Mode Configuration
Edit `mock.env` to customize mock data:
```bash
PULSE_MOCK_MODE=false # Enable/disable mock mode
PULSE_MOCK_NODES=7 # Number of mock nodes
PULSE_MOCK_VMS_PER_NODE=5 # Average VMs per node
PULSE_MOCK_LXCS_PER_NODE=8 # Average containers per node
PULSE_MOCK_RANDOM_METRICS=true # Enable metric fluctuations
PULSE_MOCK_STOPPED_PERCENT=20 # Percentage of stopped guests
```
Prefer `mock.env.local` for personal tweaks (`cp mock.env mock.env.local`). The toggle script honours `.local` first, keeping the shared defaults untouched.
## Development Workflow
1. Start hot-dev: `./scripts/hot-dev.sh`
2. Switch to mock mode for testing: `npm run mock:on`
3. Develop and test your changes
4. Switch to production mode to verify: `npm run mock:off`
5. Code changes auto-reload, no manual restarts needed!
## Troubleshooting
If the backend doesn't pick up changes:
```bash
npm run mock:off # Force restart with production data
npm run mock:on # Force restart with mock data
```
Check backend logs:
```bash
tail -f /tmp/pulse-backend.log
```
Check if services are running:
```bash
lsof -i :7656 # Backend
lsof -i :7655 # Frontend
```

View file

@ -52,7 +52,7 @@ Preferred option (no SSH keys, no proxy wiring):
Deprecated option (existing installs only):
- `pulse-sensor-proxy` is deprecated in Pulse v5 and is not recommended for new deployments.
- `pulse-sensor-proxy` is deprecated in Pulse v5 and is not recommended for new deployments. In v5, legacy sensor-proxy endpoints are disabled by default unless `PULSE_ENABLE_SENSOR_PROXY=true` is set on the Pulse server.
- Existing installs continue to work during the migration window, but plan to move to `pulse-agent --enable-proxmox`.
- Canonical temperature docs: `docs/TEMPERATURE_MONITORING.md`
@ -376,7 +376,7 @@ docker run -e API_TOKENS=ansible-token,docker-agent-token rcourtman/pulse:latest
**Security Note**: Tokens defined via environment variables are hashed with SHA3-256 before being stored on disk. Plain values never persist beyond startup.
#### Token Management (Settings → Security → API tokens)
#### 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
@ -441,7 +441,7 @@ docker run \
- 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
- Changes take effect on restart
## CORS (Cross-Origin Resource Sharing)
@ -460,14 +460,14 @@ 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.
Notes:
- `ALLOWED_ORIGINS` currently supports a single origin or `*` (it is written directly to `Access-Control-Allow-Origin`).
- Never use `ALLOWED_ORIGINS=*` in production as it allows any website to access your API.
## Monitoring and Observability
@ -566,7 +566,7 @@ curl -X POST http://localhost:7655/api/security/reset-lockout \
**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)
**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` for your domain
**Forgot password?** Start fresh delete your Pulse data and restart

View file

@ -9,9 +9,9 @@ services:
- "${PULSE_PORT:-7655}:7655"
volumes:
- pulse-data:/data
# Secure temperature monitoring via host-side proxy (requires setup - see docs)
# Uncomment after installing pulse-sensor-proxy on host with --standalone flag
# Mount is read-only (:ro) for security - proxy uses SO_PEERCRED for access control
# Temperature monitoring:
# - Recommended (v5): install pulse-agent on each Proxmox host with --enable-proxmox.
# - Deprecated: pulse-sensor-proxy (host-side proxy). If you already use it, mount the socket read-only.
# - /run/pulse-sensor-proxy:/run/pulse-sensor-proxy:ro
environment:
- TZ=${TZ:-UTC}

View file

@ -53,7 +53,7 @@ docker run -d \
<details>
<summary><strong>Advanced: OIDC / SSO</strong></summary>
Configure Single Sign-On in **Settings → Security → OIDC**, or use environment variables to lock the configuration.
Configure Single Sign-On in **Settings → Security → Single Sign-On**, or use environment variables to lock the configuration.
See [OIDC Documentation](OIDC.md) and [Proxy Auth](PROXY_AUTH.md) for details.
</details>
@ -92,6 +92,7 @@ Environment variables take precedence over `system.json`.
| `PULSE_PUBLIC_URL` | Public URL for notifications/OIDC | `""` |
| `ALLOWED_ORIGINS` | CORS allowed domains | `""` (Same origin) |
| `DISCOVERY_ENABLED` | Auto-discover nodes | `false` |
| `PULSE_ENABLE_SENSOR_PROXY` | Enable legacy `pulse-sensor-proxy` endpoints (deprecated, unsupported) | `false` |
| `PULSE_AUTH_HIDE_LOCAL_LOGIN` | Hide username/password form | `false` |
| `DEMO_MODE` | Enable read-only demo mode | `false` |

View file

@ -1,317 +0,0 @@
# Pulse v5 Documentation Audit (pre-stable)
This is a working audit of Pulse documentation as of `VERSION=5.0.0-rc.4`, focused on release readiness for a v5 stable cut.
## Status (updated 2025-12-18)
Most of the issues identified in this audit have been addressed in-repo:
- Updated install recommendation and bootstrap-token guidance across entrypoints (`README.md`, `docs/INSTALL.md`, `docs/FAQ.md`, `docs/TROUBLESHOOTING.md`, `docs/DOCKER.md`)
- Rewritten AI and API docs to match the current v5 implementation (`docs/AI.md`, `docs/API.md`)
- Rewritten metrics history docs to match SQLite store + tiered retention (`docs/METRICS_HISTORY.md`)
- Fixed adaptive polling defaults and rollout paths (`docs/monitoring/ADAPTIVE_POLLING.md`, `docs/operations/ADAPTIVE_POLLING_ROLLOUT.md`)
- Reduced temperature monitoring contradictions by making the agent the recommended path and scoping sensor-proxy as a legacy/alternative (`docs/TEMPERATURE_MONITORING.md`, `docs/security/TEMPERATURE_MONITORING.md`, `SECURITY.md`, sensor-proxy docs)
- Updated Helm/Kubernetes docs to prefer OCI distribution and flag the legacy agent block (`docs/KUBERNETES.md`, `deploy/helm/pulse/README.md`, `deploy/helm/pulse/values.yaml`)
- Added missing “operator clarity” docs (`docs/DEPLOYMENT_MODELS.md`, `docs/UPGRADE_v5.md`)
- Link validation run: no broken relative `.md` links found at time of update
## Goals
- Identify docs that are **stale**, **contradictory**, or **redundant**
- Identify **missing docs** needed for a v5 stable release
- Produce an actionable “what to change, where” checklist
## Highest-Priority Fixes (release-blockers)
### 1) Temperature monitoring guidance is contradictory
There are multiple competing “truths” about how temperature monitoring works in v5:
- `SECURITY.md` describes container deployments as requiring `pulse-sensor-proxy` and explicitly blocks SSH-based temps in containers.
- Multiple docs under `docs/security/` and `cmd/pulse-sensor-proxy/README.md` claim `pulse-sensor-proxy` is deprecated in favor of the unified agent.
- `docs/TEMPERATURE_MONITORING.md` is an extensive sensor-proxy-first guide and reads as “current”, but conflicts with the “deprecated” banner elsewhere.
- The backend still has extensive support and UX flows for sensor proxy install/register (`/api/install/install-sensor-proxy.sh`, temperature proxy diagnostics, container SSH blocking guidance).
Action:
- Decide the **canonical** v5 story:
- **Option A (agent-first)**: “Install `pulse-agent --enable-proxmox` on each Proxmox host for temperatures and management. `pulse-sensor-proxy` is legacy or edge-case only.”
- **Option B (proxy-first for containers)**: “If Pulse runs in Docker/LXC, temperatures require `pulse-sensor-proxy` (socket/HTTPS). The agent is optional for other features.”
- Update all docs to align with the chosen story, and ensure `SECURITY.md` reflects it unambiguously.
Status:
- Docs updated to be agent-first, with `pulse-sensor-proxy` treated as a legacy/alternative option.
- Remaining work is primarily product positioning and long-term deprecation decisions, not broken documentation.
Files involved:
- `SECURITY.md`
- `docs/TEMPERATURE_MONITORING.md`
- `docs/security/TEMPERATURE_MONITORING.md`
- `docs/security/SENSOR_PROXY_HARDENING.md`
- `docs/security/SENSOR_PROXY_NETWORK.md`
- `docs/security/SENSOR_PROXY_APPARMOR.md`
- `docs/operations/SENSOR_PROXY_CONFIG.md`
- `docs/operations/SENSOR_PROXY_LOGS.md`
- `cmd/pulse-sensor-proxy/README.md`
### 2) AI docs do not match the actual v5 API and configuration model
`docs/AI.md` and the AI section in `docs/API.md` appear written for an older/alternate API surface:
- `docs/AI.md` documents `PULSE_AI_PROVIDER` and `PULSE_AI_API_KEY` env vars, but the current implementation persists encrypted AI config in `ai.enc` and supports multi-provider credentials (Anthropic/OpenAI/DeepSeek/Gemini/Ollama) plus Anthropic OAuth.
- `docs/API.md` references endpoints like `POST /api/ai/chat` and `PUT /api/settings/ai` that do not match the router (current endpoints include `/api/ai/execute`, `/api/ai/models`, `/api/settings/ai/update`, OAuth endpoints, patrol stream, cost summary).
Action:
- Rewrite AI docs to match current behavior:
- Providers actually supported
- How keys/tokens are stored (encrypted) and what the UI exposes
- Anthropic OAuth flow and security implications
- Patrol and command execution (“autonomous mode”) safety controls
- Correct API endpoints and auth requirements
Files involved:
- `docs/AI.md`
- `docs/API.md`
- `internal/config/ai.go` (source of truth for config fields)
- `internal/api/router.go` (source of truth for endpoints)
Status:
- `docs/AI.md` rewritten to match multi-provider + encrypted config.
- `docs/API.md` AI endpoints updated to match router.
### 3) Installation “recommended path” is inconsistent across docs
- `README.md` recommends “Proxmox LXC (Recommended)” via GitHub `install.sh`.
- `docs/INSTALL.md` and `docs/FAQ.md` currently present Docker as the easiest/recommended path.
Action:
- Pick one recommendation hierarchy and make it consistent:
- If Proxmox LXC is the primary path, it should be the top section in `docs/INSTALL.md` and the FAQ answer should reflect it.
Files involved:
- `README.md`
- `docs/INSTALL.md`
- `docs/FAQ.md`
Status:
- Install docs now consistently present Proxmox VE LXC installer as the recommended path and include bootstrap-token retrieval.
### 4) Kubernetes/Helm docs and chart docs are out of date for v5
- `docs/KUBERNETES.md` references a chart repo URL and “Docker Agent sidecar”.
- `deploy/helm/pulse/README.md` describes “optional Docker monitoring agent” and defaults to `ghcr.io/rcourtman/pulse-docker-agent`.
Action:
- Update Helm docs to match the v5 agent direction:
- If `pulse-docker-agent` is deprecated, the chart should not reference it as primary.
- Align chart distribution instructions (Helm repo vs OCI).
Files involved:
- `docs/KUBERNETES.md`
- `deploy/helm/pulse/README.md`
- `deploy/helm/pulse/values.yaml`
- `deploy/helm/pulse/templates/*`
Status:
- `docs/KUBERNETES.md` updated to prefer OCI chart installs and flag the legacy agent block.
- `deploy/helm/pulse/README.md` and `deploy/helm/pulse/values.yaml` now label the agent workload as legacy.
## Redundant / Duplicated Docs (needs consolidation)
### Auto-update docs: two competing sources
- `docs/AUTO_UPDATE.md` describes “Settings → System Updates” and includes docker image instructions that differ from other docs.
- `docs/operations/AUTO_UPDATE.md` documents systemd timers and edits `/var/lib/pulse/system.json` which appears stale for current config defaults (`/etc/pulse/system.json`).
Action:
- Choose one canonical page (likely `docs/AUTO_UPDATE.md`) and:
- Move operational/timer details into it (or link to a clearly “advanced ops” page)
- Fix stale paths and service names
- Remove or clearly label the non-canonical duplicate
Files involved:
- `docs/AUTO_UPDATE.md`
- `docs/operations/AUTO_UPDATE.md`
Status:
- Both documents updated to current UI naming and paths; optional future work is to consolidate into a single canonical page.
### Temperature monitoring docs: two sources with different “truth”
- `docs/TEMPERATURE_MONITORING.md` (sensor proxy focused, extensive)
- `docs/security/TEMPERATURE_MONITORING.md` (agent recommended, proxy “legacy”)
Action:
- Collapse into one canonical document with a clear decision tree, then:
- Keep the other as a short redirect page, or delete it.
Files involved:
- `docs/TEMPERATURE_MONITORING.md`
- `docs/security/TEMPERATURE_MONITORING.md`
Status:
- `docs/TEMPERATURE_MONITORING.md` is now the canonical deep-dive, and `docs/security/TEMPERATURE_MONITORING.md` is a security/overview page.
### Adaptive polling docs disagree with defaults and file paths
- `docs/monitoring/ADAPTIVE_POLLING.md` claims adaptive polling is enabled by default and says env default is `true`.
- Code defaults `AdaptivePollingEnabled=false` and `docs/operations/ADAPTIVE_POLLING_ROLLOUT.md` references `/var/lib/pulse/system.json`.
Action:
- Make one canonical doc, fix defaults and paths, and ensure UI path matches current navigation.
Files involved:
- `docs/monitoring/ADAPTIVE_POLLING.md`
- `docs/operations/ADAPTIVE_POLLING_ROLLOUT.md`
Status:
- Defaults and paths updated to match current behavior.
## Stale / Incorrect Content (targeted findings)
### `docs/API.md`
Issues:
- AI endpoints mismatch current router paths (examples: `POST /api/ai/chat` vs current `/api/ai/execute`; settings update path differs).
- “complete REST API documentation” claim is optimistic. Its a curated subset plus a “check router.go” note.
Action:
- Update AI section to match `internal/api/router.go`.
- Consider splitting into:
- “Stable/public API” (guaranteed)
- “Internal/subject to change” (documented but not stable)
### `docs/METRICS_HISTORY.md`
Issues:
- Documents `PULSE_METRICS_*_RETENTION_DAYS` env vars that do not appear to exist in the server config.
- Claims metrics are stored under `/etc/pulse/data/metrics/`, but the metrics store is SQLite (`metrics.db`) under the configured data directory.
Action:
- Rewrite this doc to match the tiered retention model and actual storage format/location.
### `docs/FAQ.md`
Issues:
- Recommends Docker as easiest install, conflicts with repo README.
- Password reset guidance does not mention the bootstrap token requirement that can appear after removing `.env`.
- Mentions `METRICS_RETENTION_DAYS` which does not appear to be a current server config knob (v5 uses tiered retention settings).
Action:
- Align install recommendation with v5 positioning.
- Update auth reset steps to include bootstrap token retrieval where applicable.
- Replace metrics retention knob guidance with current retention model and UI location.
### `docs/TROUBLESHOOTING.md` and `docs/DOCKER.md`
Issues:
- “Forgot password” flow implies you can just rerun the setup wizard after deleting `.env`, but first-time setup can require the bootstrap token.
Action:
- Update password reset steps and link to the bootstrap token section in `docs/INSTALL.md`.
### `docs/RELEASE_NOTES.md`
Issues:
- Entire document is v4.x release notes.
Action:
- Replace with v5 release notes (or move to `docs/releases/` and add v5.0.0 as the top section).
- For the v5 stable cut, include: breaking changes, migration notes, and versioned “what changed since v4”.
### `cmd/pulse-sensor-proxy/README.md`
Issues:
- Mentions downloading via `/download/pulse-sensor-proxy` but the server router does not expose this endpoint.
- “Deprecated” banner conflicts with current server behavior and security guidance.
Action:
- Either bring it in line with the chosen v5 temperature story, or clearly scope it as legacy.
### Broken local link
- `docs/TEMPERATURE_MONITORING.md` contains an absolute link to `/opt/pulse/cmd/pulse-sensor-proxy/README.md` which does not work in GitHub.
Action:
- Replace with a repo-relative link (or link to the canonical temperature doc).
### Widespread “runtime path” drift (`/opt/pulse/...`)
Several user-facing docs mix:
- repository paths (`/opt/pulse/...`) used in this dev workspace, and
- runtime paths used in real installs (`/etc/pulse`, `/data`, `/var/log/pulse`, systemd units).
This creates confusion and broken copy-paste commands.
Examples to review:
- `docs/ZFS_MONITORING.md` references `/opt/pulse/.env` and `/opt/pulse/pulse.log`.
- `docs/operations/*` references `/var/lib/pulse/system.json` rather than `/etc/pulse/system.json`.
Action:
- Adopt a consistent convention across docs:
- **Runtime**: `/etc/pulse` (systemd/LXC), `/data` (Docker/Helm)
- **Repo/dev**: `/opt/pulse` only in development docs
- **Logs**: `journalctl -u pulse` (systemd) and `docker logs` (Docker), plus `/var/log/pulse/*` only if actually used in production images.
## Missing Docs for a v5 Stable Release (recommended additions)
### v5 upgrade guide (v4 → v5)
Add a single canonical page covering:
- “What changes in v5” in operator terms
- Any breaking changes and required actions
- Post-upgrade verification checklist (health endpoint, scheduler health, agents connected, temps, notifications)
- Rollback guidance for each deployment model (Docker, systemd/LXC, Helm)
Suggested path:
- `docs/UPGRADE_v5.md` (or `docs/MIGRATION_v5.md`)
### “Deployment model matrix”
Many docs implicitly assume a deployment type. Add a short matrix page that answers:
- What works on Docker vs Proxmox LXC vs systemd vs Helm
- How updates work per model
- Where config lives per model
- What “recommended” means (and why)
Suggested path:
- `docs/DEPLOYMENT_MODELS.md`
### AI safety and permissions
If v5 ships AI “execute/run-command” features:
- Document default safety posture
- What autonomous mode does
- Required scopes/roles
- Audit logging expectations
- Clear warning section for production
Suggested path:
- Expand `docs/AI.md` with a “Safety” section, or add `docs/AI_SAFETY.md`.
## Quick “Status” Inventory (what to touch for v5)
This is a fast triage list to help plan the doc refresh. Treat anything marked “Review” as “verify against current behavior”.
- Rewrite: `docs/AI.md`
- Rewrite: `docs/METRICS_HISTORY.md`
- Rewrite: `docs/RELEASE_NOTES.md` (or replace with v5 release notes)
- Update + align: `docs/INSTALL.md`, `docs/FAQ.md`, `docs/TROUBLESHOOTING.md`
- Update: `docs/API.md` (especially AI endpoints)
- Decide canonical + consolidate:
- `docs/TEMPERATURE_MONITORING.md` vs `docs/security/TEMPERATURE_MONITORING.md`
- `docs/AUTO_UPDATE.md` vs `docs/operations/AUTO_UPDATE.md`
- `docs/monitoring/ADAPTIVE_POLLING.md` vs `docs/operations/ADAPTIVE_POLLING_ROLLOUT.md`
- Review (Helm): `docs/KUBERNETES.md`, `deploy/helm/pulse/README.md`
- Review (paths): `docs/ZFS_MONITORING.md` (and any other doc that uses `/opt/pulse/...` in user instructions)
## Suggested “Doc Refresh” Execution Order
1. Decide v5 canonical stories (agent vs proxy for temps, AI capabilities, Helm strategy).
2. Update the primary entrypoints:
- `README.md`
- `docs/README.md`
- `docs/INSTALL.md`
3. Fix contradictions and remove duplicates (temperature, auto-update, adaptive polling).
4. Update `docs/API.md` to reflect current endpoints (especially AI).
5. Add v5 upgrade guide and deployment matrix.
6. Sweep FAQ + troubleshooting for the new canonical flows.

View file

@ -1,401 +0,0 @@
# Pulse Pro Implementation Plan
**Goal**: Gate AI features behind a Pro license to create a sustainable income stream.
**Timeline**: ~1-2 weeks of focused work
---
## Phase 1: License System Architecture
### 1.1 License Format (Simple JWT)
```go
// internal/license/license.go
type LicenseData struct {
LicenseID string `json:"lid"` // Unique license ID
Email string `json:"email"` // Customer email
Tier string `json:"tier"` // "pro", "msp", "enterprise"
IssuedAt time.Time `json:"iat"`
ExpiresAt time.Time `json:"exp"` // Empty = lifetime
MaxNodes int `json:"max_nodes"` // 0 = unlimited
Features []string `json:"features"` // ["ai_chat", "ai_patrol", "ai_alerts"]
}
```
**Why JWT?**
- Self-contained (no license server needed for validation)
- Signed with your private key, verified with public key embedded in binary
- Can be verified offline (important for air-gapped homelabs)
- Standard format, easy to generate from any payment processor webhook
### 1.2 License Validation Flow
```
┌────────────────────────────────────────────────────────────────┐
│ User purchases license on LemonSqueezy/Gumroad │
│ ↓ │
│ Webhook hits your simple license API (can be CloudFlare Worker)│
│ ↓ │
│ Generate JWT signed with private key │
│ ↓ │
│ Email license key to customer │
│ ↓ │
│ User pastes key in Pulse Settings → Pro tab │
│ ↓ │
│ Pulse validates signature with embedded public key │
│ ↓ │
│ Store encrypted license in config dir (license.enc) │
└────────────────────────────────────────────────────────────────┘
```
---
## Phase 2: Backend Implementation
### 2.1 New Files to Create
```
internal/license/
├── license.go # License struct, validation, JWT parsing
├── license_test.go # Tests
└── features.go # Feature flags (what Pro includes)
internal/config/
└── license.go # License persistence (store/load from disk)
```
### 2.2 License Service
```go
// internal/license/license.go
package license
import (
"crypto/ed25519"
"encoding/base64"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Embedded public key (compiled into binary)
// Generate keypair: go run ./cmd/license-keygen
var publicKeyBase64 = "YOUR_PUBLIC_KEY_HERE"
type Service struct {
license *LicenseData
loaded bool
}
func NewService() *Service {
return &Service{}
}
func (s *Service) LoadFromKey(licenseKey string) error {
// Parse and validate JWT
// Store in s.license
}
func (s *Service) IsValid() bool {
if s.license == nil {
return false
}
if !s.license.ExpiresAt.IsZero() && time.Now().After(s.license.ExpiresAt) {
return false
}
return true
}
func (s *Service) HasFeature(feature string) bool {
if !s.IsValid() {
return false
}
for _, f := range s.license.Features {
if f == feature || f == "all" {
return true
}
}
return false
}
// Feature constants
const (
FeatureAIChat = "ai_chat"
FeatureAIPatrol = "ai_patrol"
FeatureAIAlerts = "ai_alerts"
FeatureOIDC = "oidc" // SSO/OIDC authentication
FeatureKubernetes = "kubernetes" // K8s cluster monitoring
FeatureMultiUser = "multi_user" // Multiple user accounts
FeatureAPIAccess = "api_access" // Full API access for integrations
FeatureWhiteLabel = "white_label" // Custom branding (MSP tier)
FeatureAll = "all"
)
```
### 2.3 Integration Points
Modify these files to check license:
| File | What to Gate |
|------|--------------|
| `internal/api/ai_handlers.go` | Chat endpoints, patrol endpoints |
| `internal/ai/patrol.go` | Patrol service start |
| `internal/ai/service.go` | AI chat service |
| `internal/ai/alert_triggered.go` | Alert analysis |
| `internal/api/oidc_handlers.go` | OIDC/SSO configuration |
| `internal/api/kubernetes_handlers.go` | K8s cluster endpoints |
| `internal/monitoring/kubernetes/` | K8s monitoring service |
**Example gating in ai_handlers.go:**
```go
func (h *AISettingsHandler) HandleChat(w http.ResponseWriter, r *http.Request) {
// Check Pro license
if !h.licenseService.HasFeature(license.FeatureAIChat) {
utils.WriteJSONError(w, http.StatusPaymentRequired,
"AI Chat requires Pulse Pro. Visit https://pulserelay.pro to upgrade.")
return
}
// ... existing logic
}
```
---
## Phase 3: Frontend Implementation
### 3.1 New Settings Tab: "Pro License"
Location: `frontend-modern/src/routes/settings/+page.svelte` (or equivalent)
```
┌──────────────────────────────────────────────────────────────┐
│ ⚡ Pulse Pro │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ License Key │ │
│ │ [________________________________________________] │ │
│ │ [Activate License] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ✅ License Status: Active (Pro) │
│ 📧 Licensed to: user@example.com │
│ 📅 Expires: Never (Lifetime) │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Included Features: │
│ ✅ AI Chat Assistant │
│ ✅ AI Patrol (Background Health Checks) │
│ ✅ AI Alert Analysis │
│ ✅ Priority Support │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Don't have a license? │
│ [Get Pulse Pro →] https://pulserelay.pro │
│ │
└──────────────────────────────────────────────────────────────┘
```
### 3.2 Graceful Degradation for Unlicensed Users
When AI features are accessed without a license:
- **AI Settings Tab**: Show features but disabled with "Upgrade to Pro" message
- **Chat Button**: Show but with "Pro" badge, clicking prompts upgrade
- **Patrol Findings**: Hide or show "Enable with Pro" placeholder
**Don't be hostile.** The free version should still feel complete. Pro is an enhancement, not a hostage situation.
---
## Phase 4: Payment & License Generation
### 4.1 Payment Processor: LemonSqueezy
**Why LemonSqueezy over alternatives?**
- Handles global VAT/sales tax automatically
- Generates invoices (enterprises need this)
- Good webhook support for automation
- Reasonable fees (~5% + 50¢)
- Supports both subscription and one-time payments
### 4.2 Pricing Structure (Suggested)
| Tier | Price | Features | Target |
|------|-------|----------|--------|
| **Pro Monthly** | $12/month | AI features, OIDC/SSO, K8s monitoring | Individuals |
| **Pro Annual** | $99/year | Same as monthly, 2 months free | Power users |
| **Pro Lifetime** | $249 one-time | All Pro features, forever | Homelabbers who hate subscriptions |
| **MSP** | $49/month | All Pro + unlimited instances, white-label, multi-tenant | MSPs |
| **Enterprise** | Custom | All features + support SLA, on-prem license server | Large orgs |
### 4.3 Feature Matrix
| Feature | Free | Pro | MSP | Enterprise |
|---------|------|-----|-----|------------|
| Proxmox VE/PBS/PMG monitoring | ✅ | ✅ | ✅ | ✅ |
| Docker/Podman monitoring | ✅ | ✅ | ✅ | ✅ |
| Alerts (Discord, Slack, etc.) | ✅ | ✅ | ✅ | ✅ |
| Metrics history | ✅ | ✅ | ✅ | ✅ |
| Backup explorer | ✅ | ✅ | ✅ | ✅ |
| **AI Chat** | ❌ | ✅ | ✅ | ✅ |
| **AI Patrol** | ❌ | ✅ | ✅ | ✅ |
| **AI Alert Analysis** | ❌ | ✅ | ✅ | ✅ |
| **OIDC/SSO** | ❌ | ✅ | ✅ | ✅ |
| **Kubernetes monitoring** | ❌ | ✅ | ✅ | ✅ |
| Unlimited instances | ❌ | ❌ | ✅ | ✅ |
| White-label branding | ❌ | ❌ | ✅ | ✅ |
| Multi-tenant mode | ❌ | ❌ | ✅ | ✅ |
| Priority support | ❌ | Email | Email | Dedicated |
| SLA | ❌ | ❌ | ❌ | ✅ |
### 4.3 License Generation Service
A simple Cloudflare Worker or Vercel Edge Function:
```javascript
// Simplified license generator (LemonSqueezy webhook handler)
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const webhook = await request.json()
if (webhook.meta.event_name === 'order_created') {
const license = generateLicense({
email: webhook.data.attributes.user_email,
tier: 'pro',
expiresAt: null, // lifetime for now
})
// Send license via email
await sendLicenseEmail(webhook.data.attributes.user_email, license)
}
return new Response('OK')
}
function generateLicense(data) {
// Sign JWT with private key
// Return base64-encoded license key
}
```
---
## Phase 5: Launch Communication
### 5.1 Changelog Entry
```markdown
## v5.0.0 - The AI Update
### 🚀 Major Changes
**Introducing Pulse Pro**
Pulse 5.0 includes powerful AI features that require a Pro license:
- **AI Chat**: Natural language interface to your infrastructure
- **AI Patrol**: Background health monitoring and insights
- **AI Alert Analysis**: Smart analysis when alerts fire
Core monitoring features remain **completely free and open source**.
Pro licenses support ongoing development and enable me to work on Pulse full-time.
[Get Pulse Pro →](https://pulserelay.pro)
---
*Pulse has grown from a weekend project to something used by thousands.
To keep improving it, I need to make it sustainable. Thank you for your support!*
— Richard
```
### 5.2 Preemptive FAQ
Add to README or docs:
**Q: Why are AI features paid?**
A: AI features require significant development effort and ongoing maintenance. Pro licenses let me work on Pulse sustainably while keeping core monitoring free.
**Q: Will monitoring features become paid?**
A: No. Proxmox/Docker/K8s monitoring, alerts, history, and all current free features will remain free forever.
**Q: What if I'm already using AI in the RC?**
A: Thank you for testing! RC users were beta testers helping shape these features. The final release requires a Pro license.
**Q: I can't afford Pro.**
A: Email me (richard@pulserelay.pro). I offer discounts for students, hobbyists in financial hardship, and open source contributors.
**Q: Can I self-host without Pro?**
A: Absolutely. Pulse works great without AI features. Pro is optional.
---
## Implementation Order
1. **Week 1: Backend**
- [ ] Create `internal/license/` package
- [ ] Implement JWT validation with embedded public key
- [ ] Add license persistence (encrypted storage)
- [ ] Gate AI endpoints with license checks
- [ ] Add `/api/license` endpoints (check, activate)
2. **Week 2: Frontend + Payment**
- [ ] Add Pro License settings tab
- [ ] Update AI settings to show Pro-gated state
- [ ] Set up LemonSqueezy product
- [ ] Create license generation webhook
- [ ] Set up pulserelay.pro landing page (can be simple)
- [ ] Write announcement blog post
3. **Launch**
- [ ] Release v5.0.0 stable
- [ ] Post to Reddit (/r/homelab, /r/Proxmox, /r/selfhosted)
- [ ] Post to GitHub Discussions
- [ ] Email mailing list (if you have one)
---
## Security Considerations
1. **Private key**: Never commit to repo. Store in password manager + secure backup.
2. **License validation**: Always verify signature, never trust claims without verification.
3. **Obfuscation**: Consider light obfuscation of license check code (not for security, but to discourage trivial patching).
4. **Grace period**: If validation fails, maybe grant 7-day grace period before disabling (better UX).
---
## What NOT to Do
- ❌ Phone-home license validation (breaks air-gapped installs)
- ❌ Aggressive license enforcement (pisses off users)
- ❌ Remove free features to "encourage" upgrades
- ❌ Make the free version feel crippled
- ❌ Hide that it's paid (be upfront in README)
---
## Success Metrics (First 90 Days)
| Metric | Target |
|--------|--------|
| Pro licenses sold | 50-100 |
| Monthly revenue | $500-$1000 |
| Churn rate | <5% |
| Negative community reactions | <10 vocal complaints |
| GitHub stars lost | <50 |
If you hit these numbers, you've validated the model. Then you can expand to MSP tier, add features, etc.
---
*This plan can be adjusted based on your preferences. Want me to help implement any specific part?*

View file

@ -42,16 +42,14 @@ Welcome to the Pulse documentation portal. Here you'll find everything you need
- **[Unified Agent](UNIFIED_AGENT.md)** Single binary for Host and Docker monitoring.
- **[VM Disk Monitoring](VM_DISK_MONITORING.md)** Enabling QEMU Guest Agent for disk stats.
- **[Temperature Monitoring](TEMPERATURE_MONITORING.md)** Setting up hardware sensors.
- **[Temperature Monitoring](TEMPERATURE_MONITORING.md)** Agent-based temperature monitoring (`pulse-agent --enable-proxmox`). Sensor proxy is deprecated in v5.
- **[Webhooks](WEBHOOKS.md)** Custom notification payloads.
## 💻 Development
- **[API Reference](API.md)** Complete REST API documentation.
- **[Dev Quick Start](../DEV-QUICK-START.md)** Hot-reload workflow for contributors.
- **[Architecture](../ARCHITECTURE.md)** System design and component interaction.
- **[Contributing](../CONTRIBUTING.md)** How to contribute to Pulse.
- **[Mock Mode](development/MOCK_MODE.md)** Developing without real infrastructure.
---

View file

@ -1,631 +0,0 @@
# Security Audit Report - Pulse Temperature Proxy
## Date: 2025-11-07
## Auditors: Claude (Sonnet 4.5) + Codex
---
## Executive Summary
This document presents the findings and remediations from a comprehensive security audit of the pulse-sensor-proxy architecture. The audit identified **9 security issues** ranging from critical to low severity, all of which have been **successfully remediated**.
### Overall Assessment
**Before Audit:** B+ (Good, with important gaps)
**After Remediation:** A (Excellent security posture)
**Risk Reduction:** All critical vulnerabilities eliminated. System now resilient to container compromise scenarios.
---
## Audit Methodology
1. **Architecture Review** - Analysis of trust boundaries and security design
2. **Code Review** - Line-by-line examination of security-critical code paths
3. **Threat Modeling** - Evaluation of attack vectors and exploitation scenarios
4. **Collaborative Analysis** - Claude + Codex independent review and challenge
5. **Implementation** - Codex-led implementation of all fixes
6. **Testing** - Comprehensive test coverage for all security features
---
## Findings and Remediations
### CRITICAL SEVERITY
#### 1. Socket Directory Tampering ✅ FIXED
**Finding:**
Socket directory mounted read-write into containers, allowing compromised containers to:
- Unlink socket and create man-in-the-middle proxies
- Fill `/run/pulse-sensor-proxy/` to exhaust tmpfs
- Race proxy service on restart to hijack socket path
**CVSS Score:** 8.1 (High)
**Attack Complexity:** Low
**Privileges Required:** Low (container access)
**Impact:** Complete compromise of proxy communication
**Remediation:**
- Changed all socket mounts to read-only (`:ro`)
- Updated documentation to reflect secure configuration
- Added validation to installer
**Files Modified:**
- `docker-compose.yml`
- `docs/TEMPERATURE_MONITORING.md`
**Status:** ✅ Deployed
---
#### 2. Unrestricted SSRF via get_temperature ✅ FIXED
**Finding:**
Proxy would SSH to ANY hostname/IP passing format validation, enabling:
- Internal network reconnaissance via SSH handshakes
- Port scanning using proxy as relay
- Resource exhaustion via slow-loris SSH attacks
- Complete bypass of network security controls
**CVSS Score:** 8.6 (High)
**Attack Complexity:** Low
**Privileges Required:** Low (container access)
**Impact:** Full internal network access from host context
**Remediation:**
- Implemented multi-layer node validation system
- Configurable `allowed_nodes` list (hostnames, IPs, CIDR ranges)
- Automatic cluster membership validation on Proxmox hosts
- 5-minute cache of cluster membership
- New metric: `pulse_proxy_node_validation_failures_total`
**Configuration Example:**
```yaml
allowed_nodes:
- "pve1"
- "192.168.1.0/24"
strict_node_validation: true
```
**Files Created:**
- `cmd/pulse-sensor-proxy/validation.go`
- `cmd/pulse-sensor-proxy/validation_test.go`
**Files Modified:**
- `cmd/pulse-sensor-proxy/config.go`
- `cmd/pulse-sensor-proxy/main.go`
- `cmd/pulse-sensor-proxy/metrics.go`
**Tests:** 4 new tests, all passing
**Status:** ✅ Deployed
---
#### 3. Missing Read Deadline (Connection Exhaustion) ✅ FIXED
**Finding:**
No read deadline allowed attackers to hold connection slots indefinitely:
- Connect but never send data
- 4 UIDs could consume all 8 global slots
- Trivial DoS with minimal resources
**CVSS Score:** 7.5 (High)
**Attack Complexity:** Low
**Privileges Required:** Low (container access)
**Impact:** Complete service denial
**Remediation:**
- Added configurable `read_timeout` (default 5s) and `write_timeout` (default 10s)
- Read deadline set before request parsing, cleared before handler
- Write deadline set before response transmission
- Automatic penalty on timeout
- New metrics: `pulse_proxy_read_timeouts_total`, `pulse_proxy_write_timeouts_total`
**Configuration:**
```yaml
read_timeout: 5s
write_timeout: 10s
```
**Files Modified:**
- `cmd/pulse-sensor-proxy/config.go`
- `cmd/pulse-sensor-proxy/main.go`
- `cmd/pulse-sensor-proxy/metrics.go`
**Status:** ✅ Deployed
---
#### 4. Multi-UID Rate Limit Bypass ✅ FIXED
**Finding:**
Rate limiting per-UID easily bypassed by creating multiple users in container:
- Each user mapped to unique host UID (100000-165535 range)
- Each UID got separate rate limit quota
- Attackers could drive proxy to 100% CPU
**CVSS Score:** 7.5 (High)
**Attack Complexity:** Low
**Privileges Required:** Low (container access)
**Impact:** Service degradation/denial
**Remediation:**
- Automatic detection of ID-mapped UID ranges from `/etc/subuid` and `/etc/subgid`
- Rate limits applied per-range for container UIDs
- Rate limits applied per-UID for host UIDs (backwards compatible)
- Metrics show `peer="range:100000-165535"` or `peer="uid:0"`
**Technical Implementation:**
- `identifyPeer()` checks if BOTH UID AND GID are in mapped ranges
- If in range: all UIDs share rate limits
- If NOT in range: legacy per-UID limiting
**Files Modified:**
- `cmd/pulse-sensor-proxy/throttle.go`
- `cmd/pulse-sensor-proxy/main.go`
- `cmd/pulse-sensor-proxy/auth.go`
- `cmd/pulse-sensor-proxy/metrics.go`
**Files Created:**
- `cmd/pulse-sensor-proxy/throttle_test.go`
**Tests:** 1 new test, passing
**Status:** ✅ Deployed
---
### MEDIUM SEVERITY
#### 5. Incomplete GID Authorization ✅ FIXED
**Finding:**
`allowed_peer_gids` populated from config but never checked during authorization:
- Created false sense of security
- GID-based policies silently ignored
- Administrators unaware policies not enforced
**CVSS Score:** 5.3 (Medium)
**Attack Complexity:** Low
**Impact:** Authorization bypass
**Remediation:**
- Implemented GID checking in `authorizePeer()`
- Peer authorized if UID **OR** GID matches
- Debug logging shows which rule granted access
- Updated documentation
**Files Modified:**
- `cmd/pulse-sensor-proxy/auth.go`
**Files Created:**
- `cmd/pulse-sensor-proxy/auth_test.go`
**Tests:** 2 new tests, all passing
**Status:** ✅ Deployed
---
#### 6. Unbounded SSH Output ✅ FIXED
**Finding:**
No limit on SSH command output size:
- Malicious remote node could stream gigabytes
- Memory exhaustion
- CPU spike on parsing
**CVSS Score:** 6.5 (Medium)
**Attack Complexity:** Low
**Impact:** Resource exhaustion
**Remediation:**
- Added `max_ssh_output_bytes` config (default: 1MB)
- Stream with `io.LimitReader` to cap output size
- Error if limit exceeded
- New metric: `pulse_proxy_ssh_output_oversized_total{node}`
- WARN logging for oversized outputs
**Configuration:**
```yaml
max_ssh_output_bytes: 1048576 # 1MB default
```
**Files Modified:**
- `cmd/pulse-sensor-proxy/config.go`
- `cmd/pulse-sensor-proxy/ssh.go`
- `cmd/pulse-sensor-proxy/metrics.go`
**Files Created:**
- `cmd/pulse-sensor-proxy/ssh_test.go`
**Tests:** 1 new test, passing
**Status:** ✅ Deployed
---
#### 7. Weak Host Key Validation (TOFU) ✅ FIXED
**Finding:**
Trust-On-First-Use (TOFU) via `ssh-keyscan`:
- Trusts whatever key remote offers on first contact
- No administrator approval for new fingerprints
- Vulnerable to MITM if container influences routing
**CVSS Score:** 6.5 (Medium)
**Attack Complexity:** Medium
**Impact:** MITM attacks possible
**Remediation:**
- Implemented Proxmox host key seeding from `/etc/pve/priv/known_hosts`
- Falls back to ssh-keyscan only if Proxmox unavailable (with WARN)
- Added `require_proxmox_hostkeys` config option
- Fingerprint change detection with ERROR logging
- New metric: `pulse_proxy_hostkey_changes_total{node}`
**Configuration:**
```yaml
require_proxmox_hostkeys: false # true = strict mode
```
**Files Modified:**
- `internal/ssh/knownhosts/manager.go`
- `cmd/pulse-sensor-proxy/ssh.go`
- `cmd/pulse-sensor-proxy/config.go`
- `cmd/pulse-sensor-proxy/metrics.go`
**Files Created:**
- `internal/ssh/knownhosts/manager_test.go`
**Tests:** 7 new tests, all passing
**Status:** ✅ Deployed
---
#### 8. Insufficient Capability Separation ✅ FIXED
**Finding:**
Any UID in `allowed_peer_uids` could call privileged methods:
- No separation between read-only and admin capabilities
- If another service's UID in allowlist, inherits full control
**CVSS Score:** 6.5 (Medium)
**Attack Complexity:** Low
**Impact:** Privilege escalation
**Remediation:**
- Implemented capability-based authorization system
- Three capability levels: `read`, `write`, `admin`
- Per-UID capability assignment
- Privileged methods require `admin` capability
- Backwards compatible with legacy config
**Configuration:**
```yaml
allowed_peers:
- uid: 0
capabilities: [read, write, admin] # Root gets all
- uid: 1000
capabilities: [read] # Docker: read-only
- uid: 1001
capabilities: [read, write] # Can call temps but not key distribution
```
**Files Created:**
- `cmd/pulse-sensor-proxy/capabilities.go`
**Files Modified:**
- `cmd/pulse-sensor-proxy/config.go`
- `cmd/pulse-sensor-proxy/auth.go`
- `cmd/pulse-sensor-proxy/main.go`
**Tests:** 1 new test, passing
**Status:** ✅ Deployed
---
### LOW SEVERITY
#### 9. Missing Systemd Hardening ✅ FIXED
**Finding:**
Additional systemd hardening options available but not enabled:
- `MemoryDenyWriteExecute` (prevents RWX memory)
- `RestrictRealtime` (denies realtime scheduling)
- `ProtectHostname` (hostname protection)
- `ProtectKernelLogs` (kernel log protection)
- `SystemCallArchitectures` (native only)
**CVSS Score:** 3.1 (Low)
**Attack Complexity:** High
**Impact:** Defense in depth
**Remediation:**
- Added all missing hardening directives
- Verified compatibility with Go runtime
- Updated systemd unit file
**Files Modified:**
- `scripts/pulse-sensor-proxy.service`
**Status:** ✅ Deployed
---
## New Security Features
### Enhanced Metrics
All new security features include Prometheus metrics:
| Metric | Purpose |
|--------|---------|
| `pulse_proxy_node_validation_failures_total{node, reason}` | SSRF attempt detection |
| `pulse_proxy_read_timeouts_total` | Connection DoS detection |
| `pulse_proxy_write_timeouts_total` | Write timeout tracking |
| `pulse_proxy_limiter_rejections_total{peer, reason}` | Rate limit monitoring |
| `pulse_proxy_limiter_penalties_total{peer, reason}` | Penalty tracking |
| `pulse_proxy_global_concurrency_inflight` | Concurrency monitoring |
| `pulse_proxy_ssh_output_oversized_total{node}` | Output size violations |
| `pulse_proxy_hostkey_changes_total{node}` | Fingerprint changes |
### Improved Logging
- Node validation failures: WARN with "potential SSRF attempt"
- Read timeouts: WARN with "slow client or attack"
- Fingerprint changes: ERROR level
- All events include correlation IDs
- Peer labels show "range:X-Y" for containers
### Configuration Flexibility
All features configurable via:
- YAML config file (`/etc/pulse-sensor-proxy/config.yaml`)
- Environment variables (e.g., `PULSE_SENSOR_PROXY_READ_TIMEOUT`)
- Command-line flags
---
## Testing Summary
### Test Coverage
**Total New Tests:** 17
**All Tests Passing:** ✅ Yes
**Test Breakdown:**
- Node validation: 4 tests
- Authorization: 3 tests
- Rate limiting: 1 test
- SSH output limits: 1 test
- Host key management: 7 tests
- Capability system: 1 test
### Build Verification
```bash
✅ All tests pass: go test ./cmd/pulse-sensor-proxy ./internal/ssh/knownhosts
✅ Binary builds: ./pulse-sensor-proxy-hardened
✅ Configuration validated
✅ Systemd unit verified
```
---
## Deployment Guide
### Breaking Changes
1. **Socket mounts MUST be changed to `:ro`** (security fix)
```yaml
# OLD:
- /run/pulse-sensor-proxy:/run/pulse-sensor-proxy:rw
# NEW:
- /run/pulse-sensor-proxy:/run/pulse-sensor-proxy:ro
```
2. **Containers with multiple users now share rate limits** (security fix, prevents bypass)
### Migration Steps
1. **Update Configuration**
Create `/etc/pulse-sensor-proxy/config.yaml`:
```yaml
# Node allowlist (prevents SSRF)
allowed_nodes:
- "10.0.0.0/24" # Your cluster network
strict_node_validation: true
# Timeouts (prevents DoS)
read_timeout: 5s
write_timeout: 10s
# SSH output limits
max_ssh_output_bytes: 1048576 # 1MB
# Host key management
require_proxmox_hostkeys: false # Set true for strict mode
# Capability-based authorization
allowed_peers:
- uid: 0
capabilities: [read, write, admin]
- uid: 1000
capabilities: [read] # Docker containers: read-only
```
2. **Update Socket Mounts**
Docker:
```bash
# Edit docker-compose.yml
sed -i 's/:rw$/:ro/g' docker-compose.yml
docker compose down && docker compose up -d
```
LXC:
```bash
# Mounts created by install script are already correct
# Verify: pct config <VMID> | grep mp
```
3. **Restart Proxy**
```bash
systemctl restart pulse-sensor-proxy
```
4. **Update Monitoring**
Add Prometheus alerts:
```yaml
groups:
- name: pulse-sensor-proxy-security
rules:
# SSRF attempts
- alert: PulseSensorSSRFAttempt
expr: rate(pulse_proxy_node_validation_failures_total[5m]) > 0
labels:
severity: warning
annotations:
summary: "SSRF attempt blocked on {{ $labels.instance }}"
# Read timeout attacks
- alert: PulseSensorReadTimeouts
expr: rate(pulse_proxy_read_timeouts_total[5m]) > 1
labels:
severity: warning
annotations:
summary: "High read timeout rate on {{ $labels.instance }}"
# Fingerprint changes
- alert: PulseSensorHostKeyChange
expr: increase(pulse_proxy_hostkey_changes_total[1h]) > 0
labels:
severity: critical
annotations:
summary: "SSH host key changed for {{ $labels.node }}"
```
### Backwards Compatibility
**Preserved:**
- Empty `allowed_nodes` + Proxmox host = auto-validate cluster
- Empty `allowed_nodes` + non-Proxmox = allow all (legacy)
- Host UID rate limiting unchanged
- Legacy `allowed_peer_uids` format still works (grants all capabilities)
**Changed (intentionally):**
- Socket mounts now `:ro` (security fix)
- Container UIDs now share rate limits (security fix)
---
## Security Posture Comparison
| Attack Vector | Before | After | Improvement |
|---------------|--------|-------|-------------|
| **SSRF** | ❌ Trivially exploitable | ✅ Eliminated | Node validation |
| **Connection DoS** | ❌ 4 UIDs = full starvation | ✅ Eliminated | Read deadlines |
| **Multi-UID Bypass** | ❌ 100+ UIDs available | ✅ Eliminated | Range-based limiting |
| **Socket Tampering** | ❌ Container can MITM | ✅ Eliminated | Read-only mount |
| **GID Policy Bypass** | ❌ Silently ignored | ✅ Enforced | GID checking |
| **Memory Exhaustion** | ❌ Unbounded SSH output | ✅ Mitigated | Output limits |
| **MITM Attacks** | ⚠️ TOFU vulnerable | ✅ Improved | Proxmox key seeding |
| **Privilege Escalation** | ⚠️ UID = full admin | ✅ Controlled | Capability system |
| **Process Exploitation** | ⚠️ Basic hardening | ✅ Hardened | Systemd directives |
---
## Risk Assessment
### Before Audit
**Critical Risks:**
- Container compromise → Full internal network SSRF
- Container compromise → Trivial service DoS
- Container compromise → Rate limit bypass
- Container compromise → Proxy MITM
**Overall Risk Level:** HIGH
### After Remediation
**Residual Risks:**
- Proxy binary compromise → SSH key access (unavoidable given architecture)
- Zero-day in Go runtime or dependencies
- Social engineering / operator error
**Overall Risk Level:** LOW
**Risk Reduction:** 85%+ reduction in exploitable attack surface
---
## Recommendations for Production
### Immediate Actions
1. ✅ Deploy all security fixes (all completed)
2. ✅ Update socket mounts to read-only
3. ✅ Configure node allowlists
4. ✅ Enable monitoring/alerting
### Ongoing Security
1. **Regular audits** - Annual security reviews
2. **Dependency updates** - Monitor Go/SSH library security advisories
3. **Log monitoring** - Watch for validation failures and timeouts
4. **Key rotation** - Use existing rotation script quarterly
5. **Incident response** - Document and practice response procedures
### Future Enhancements
1. **Strict host key mode** - Require administrator approval for new fingerprints
2. **TLS for metrics** - Encrypt metrics endpoint
3. **Advanced rate limiting** - Adaptive throttling based on behavior
4. **Extended audit logging** - Structured audit logs with retention
---
## Conclusion
The pulse-sensor-proxy architecture underwent comprehensive security hardening, addressing all identified vulnerabilities. The system now demonstrates:
- **Defense in Depth:** Multiple layers of security controls
- **Least Privilege:** Capability-based authorization
- **Attack Resilience:** DoS-resistant design
- **SSRF Prevention:** Complete node validation
- **Container Isolation:** Read-only mounts, range-based limiting
- **Monitoring:** Comprehensive security telemetry
**Final Security Grade:** A (Excellent)
The proxy is now production-ready for security-sensitive deployments.
---
## References
- **Security Changelog:** `docs/SECURITY_CHANGELOG.md`
- **Hardening Guide:** `docs/PULSE_SENSOR_PROXY_HARDENING.md`
- **Security Architecture:** `docs/TEMPERATURE_MONITORING_SECURITY.md`
- **Configuration Guide:** `docs/CONFIGURATION.md`
---
## Audit Team
**Lead Auditor:** Claude (Anthropic Sonnet 4.5)
**Implementation:** OpenAI Codex
**Methodology:** Collaborative adversarial analysis
**Audit Duration:** 2025-11-07 (single day comprehensive audit)
**Lines of Code Reviewed:** ~5,000
**Security Issues Found:** 9
**Issues Remediated:** 9 (100%)
---
**For security concerns or questions:**
https://github.com/rcourtman/Pulse/issues

View file

@ -1,375 +0,0 @@
# Security Audit Report - Pulse Application
## Date: 2025-12-18
## Auditor: Claude (Gemini)
---
## Executive Summary
This document presents the findings from a comprehensive security audit of the Pulse monitoring application. The audit examined authentication, authorization, cryptography, input validation, SSRF prevention, command execution, and general security practices.
### Overall Assessment
**Security Posture: A- (Excellent with minor recommendations)**
The codebase demonstrates a mature security posture with:
- ✅ Strong cryptographic practices (bcrypt, SHA3-256, AES-256-GCM)
- ✅ Comprehensive SSRF protection for webhooks
- ✅ CSRF protection for session-based authentication
- ✅ Rate limiting and account lockout
- ✅ Command execution policy with blocklist/allowlist
- ✅ Proper input sanitization and validation
- ✅ Security headers implementation
- ✅ Audit logging
A prior security audit on 2025-11-07 addressed 9 critical to low severity issues in the sensor-proxy component, all of which were successfully remediated.
---
## Audit Scope
### Components Reviewed
1. **Authentication System** (`internal/api/auth.go`, `internal/auth/`)
2. **Session Management** (`internal/api/security.go`, session stores)
3. **Cryptography** (`internal/crypto/crypto.go`)
4. **API Token Management** (`internal/api/security_tokens.go`)
5. **OIDC Integration** (`internal/api/security_oidc.go`)
6. **Webhook/Notification Security** (`internal/notifications/`)
7. **Command Execution** (`internal/agentexec/policy.go`)
8. **Database Operations** (`internal/metrics/store.go`)
9. **Configuration & Secrets** (`internal/config/`)
---
## Strengths Identified
### 1. Authentication & Password Security ✅
- **bcrypt hashing** with cost factor 12 for passwords
- **SHA3-256** for API token hashing
- **Constant-time comparison** for token validation (prevents timing attacks)
- **12-character minimum** password length requirement
- **Automatic hashing** of plain-text passwords on startup
### 2. Session Security ✅
- **HttpOnly cookies** for session tokens
- **Secure flag** set based on HTTPS detection
- **SameSite policy** properly configured (Lax/None based on proxy detection)
- **24-hour session expiry** with sliding window extension
- **Session invalidation** on password change
### 3. Rate Limiting & Account Lockout ✅
- **10 attempts/minute** for auth endpoints
- **5 failed attempts** triggers 15-minute lockout
- **Per-username AND per-IP** tracking
- **Lockout bypass prevention** (both must be clear)
### 4. CSRF Protection ✅
- **CSRF tokens** generated per session
- **Separate CSRF cookie** (not HttpOnly, readable by JS)
- **Header/form validation** for state-changing requests
- **Safe methods** (GET, HEAD, OPTIONS) exempted
- **API token auth** correctly bypasses CSRF (not vulnerable)
### 5. SSRF Prevention ✅
- **Webhook URL validation** with DNS resolution check
- **Private IP blocking** (RFC1918, link-local, loopback)
- **Cloud metadata endpoint blocking** (169.254.169.254, etc.)
- **Configurable allowlist** for internal webhooks
- **DNS rebinding protection** via IP resolution verification
### 6. Encryption at Rest ✅
- **AES-256-GCM** for credential encryption
- **Unique nonce** generation per encryption operation
- **Key file protections** with existence validation before encryption
- **Orphaned data prevention** (refuses to encrypt if key deleted)
### 7. Security Headers ✅
- Content-Security-Policy
- X-Frame-Options (DENY by default)
- X-Content-Type-Options: nosniff
- X-XSS-Protection
- Referrer-Policy
- Permissions-Policy
### 8. Command Execution Policy ✅
- **Blocklist** for dangerous commands (rm -rf, mkfs, dd, etc.)
- **Auto-approve list** for read-only inspection commands
- **Require approval** for service control, package management
- **Sudo normalization** for consistent policy application
### 9. SQL Injection Prevention ✅
- **Parameterized queries** used throughout metrics store
- **Prepared statements** for batch operations
- No string concatenation in SQL queries
### 10. XSS Prevention ✅
- **DOMPurify** for markdown rendering
- **HTML entity encoding** in tooltips
- **Allowed tag/attribute lists** for sanitized content
- **LLM output sanitization** (AI chat)
---
## Findings & Recommendations
### HIGH SEVERITY: None Identified
### MEDIUM SEVERITY
#### M1. Admin Bypass Debug Mode 🟡
**Location:** `internal/api/auth.go:675-691`
**Finding:**
The `adminBypassEnabled()` function allows bypassing authentication when both `ALLOW_ADMIN_BYPASS=1` and `PULSE_DEV=true` are set. While properly gated for development only:
```go
if os.Getenv("ALLOW_ADMIN_BYPASS") != "1" {
return
}
if os.Getenv("PULSE_DEV") == "true" || strings.EqualFold(os.Getenv("NODE_ENV"), "development") {
log.Warn().Msg("Admin authentication bypass ENABLED (development mode)")
adminBypassState.enabled = true
}
```
**Risk:** Accidental production deployment with these env vars could expose full admin access.
**Recommendation:**
1. Add prominent warning log at startup if either var is set
2. Consider disallowing in Docker `PULSE_DOCKER=true` mode
3. Document this explicitly as a development-only feature
---
#### M2. Recovery Token Exposure Window 🟡
**Location:** Session and recovery token stores
**Finding:**
Recovery tokens for password reset appear to be stored in JSON files. While tokens are hashed:
- File permissions should be verified as 0600
- Token expiration should be enforced server-side (appears to be implemented)
**Recommendation:**
1. Verify file permissions are set correctly (0600) during token store initialization
2. Add cleanup routine for expired tokens
---
### LOW SEVERITY
#### L1. Cookie Security in HTTP Proxies 🔵
**Location:** `internal/api/auth.go:75-107`
**Finding:**
When behind an HTTP (non-HTTPS) proxy, cookies fall back to `SameSite=Lax` with `Secure=false`. This is functionally necessary but reduces security.
**Recommendation:**
1. Log a warning when cookies are set without Secure flag
2. Add documentation recommending HTTPS termination at proxy
---
#### L2. Session Token Entropy 🔵
**Location:** `internal/api/auth.go:109-118`
**Finding:**
Session tokens are 32 bytes (256 bits) of entropy via `crypto/rand`, which is excellent. However, the error handling falls back to empty string:
```go
if _, err := cryptorand.Read(b); err != nil {
log.Error().Err(err).Msg("Failed to generate secure session token")
return "" // Fallback - should never happen
}
```
**Recommendation:**
Consider returning an error or panicking rather than returning empty string, as an empty session token could have undefined behavior.
---
#### L3. OIDC State Parameter Validation 🔵
**Location:** `internal/api/security_oidc.go`
**Finding:**
OIDC configuration is properly validated and state parameters should be verified during the OAuth flow. This should be confirmed in the callback handler.
**Recommendation:**
1. Verify state parameter is generated with sufficient entropy
2. Ensure state parameter has short expiration (5-10 minutes)
---
#### L4. Apprise CLI Command Execution 🔵
**Location:** `internal/notifications/notifications.go`
**Finding:**
The Apprise CLI path and targets are passed to `exec.CommandContext`. While the CLI path is configurable:
```go
args := []string{"-t", title, "-b", body}
args = append(args, cfg.Targets...)
execFn := n.appriseExec
```
**Risk:** If an attacker can control `cfg.Targets`, they might inject malicious arguments.
**Recommendation:**
1. Validate that targets match expected Apprise URL format
2. Consider sanitizing or escaping special characters in targets
---
### INFORMATIONAL
#### I1. Dependencies
The `go.mod` shows modern, well-maintained dependencies:
- Go 1.24.0 (latest stable)
- `golang.org/x/crypto v0.45.0` (current)
- `github.com/coreos/go-oidc/v3 v3.17.0` (current)
**Recommendation:**
Run `govulncheck` periodically to scan for known vulnerabilities.
---
#### I2. GitGuardian Integration
The `.gitguardian.yaml` is properly configured to:
- Ignore documentation and example files
- Block placeholder patterns
- Scan actual code and configuration
---
#### I3. Existing Security Audit
The previous audit (2025-11-07) addressed critical vulnerabilities in the sensor-proxy:
- Socket directory tampering (CRITICAL) ✅ Fixed
- SSRF via get_temperature (CRITICAL) ✅ Fixed
- Connection exhaustion DoS (CRITICAL) ✅ Fixed
- Multi-UID rate limit bypass (CRITICAL) ✅ Fixed
- Incomplete GID authorization (MEDIUM) ✅ Fixed
- Unbounded SSH output (MEDIUM) ✅ Fixed
- Weak host key validation (MEDIUM) ✅ Fixed
- Insufficient capability separation (MEDIUM) ✅ Fixed
- Missing systemd hardening (LOW) ✅ Fixed
---
## Security Architecture Summary
### Data Flow Security
```
┌─────────────────────────────────────────────────────────────────┐
│ Client (Browser/API) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ HTTPS + TLS │ Session Cookie (HttpOnly, Secure, SameSite) ││
│ │ │ CSRF Token (Cookie + Header validation) ││
│ │ │ API Token (Header: X-API-Token) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Pulse Server │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Rate Limiter │ │ Auth Middleware │ │ CSRF Handler │ │
│ │ 10 auth/min │ │ Session/Token │ │ State-changing │ │
│ │ 500 api/min │ │ Validation │ │ operations │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Account Lockout │ │ Command Policy │ │ SSRF Prevention │ │
│ │ 5 attempts/15m │ │ Block/Allow/ │ │ Private IP │ │
│ │ │ │ Require Approval│ │ blocklist │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Encryption at Rest (AES-256-GCM) ││
│ │ - Node credentials: /etc/pulse/nodes.enc ││
│ │ - Email settings: /etc/pulse/email.enc ││
│ │ - Webhooks: /etc/pulse/webhooks.enc ││
│ │ - OIDC config: /etc/pulse/oidc.enc ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
```
### Password/Token Hashing
| Credential Type | Algorithm | Parameters |
|-----------------|-----------|------------|
| User Passwords | bcrypt | Cost factor 12 |
| API Tokens | SHA3-256 | - |
| Encryption Key | AES-256-GCM | 32-byte random key |
| Session Tokens | Random | 32 bytes (256-bit) |
---
## Compliance Checklist
| Requirement | Status | Notes |
|-------------|--------|-------|
| Password hashing | ✅ | bcrypt, cost 12 |
| Session management | ✅ | Secure cookies, 24h expiry |
| CSRF protection | ✅ | Token-based |
| Rate limiting | ✅ | Auth + API endpoints |
| Encryption at rest | ✅ | AES-256-GCM |
| HTTPS support | ✅ | TLS configurable |
| Security headers | ✅ | CSP, X-Frame-Options, etc. |
| Audit logging | ✅ | Auth events logged |
| Input validation | ✅ | SQL params, webhook URLs |
| Command execution control | ✅ | Policy-based |
---
## Recommendations Summary
### Priority 1 (Consider Addressing)
- [ ] M1: Add additional safeguards for dev mode bypass
- [ ] M2: Verify recovery token file permissions
### Priority 2 (Optional Improvements)
- [ ] L1: Add warning logs for non-secure cookies
- [ ] L2: Improve session token generation error handling
- [ ] L3: Document OIDC state parameter security
- [ ] L4: Add Apprise target validation
### Priority 3 (Ongoing)
- [ ] I1: Run `govulncheck` regularly
- [ ] Keep dependencies updated
- [ ] Review GitGuardian alerts
---
## Conclusion
The Pulse application demonstrates a **strong security posture** with comprehensive protections against common web application vulnerabilities. The codebase shows evidence of security-conscious development practices:
1. **Defense in depth** with multiple layers of authentication and authorization
2. **Secure defaults** requiring explicit configuration to reduce security
3. **Modern cryptography** using industry-standard algorithms
4. **Comprehensive validation** of user inputs and external URLs
5. **Audit trails** for security-relevant events
The identified findings are primarily of low to medium severity and represent opportunities for hardening rather than critical vulnerabilities.
**Final Security Grade: A-**
---
## References
- **Previous Audit:** `docs/SECURITY_AUDIT_2025-11-07.md`
- **Security Policy:** `SECURITY.md`
- **Security Changelog:** `docs/SECURITY_CHANGELOG.md`
---
## Audit Team
**Auditor:** Claude (Gemini 2.5)
**Methodology:** Static code analysis and architecture review
**Audit Duration:** 2025-12-18 (single session)
**Files Reviewed:** ~50 source files across 10 packages
---
**For security concerns or questions:**
https://github.com/rcourtman/Pulse/issues

View file

@ -374,8 +374,6 @@ go build ./cmd/pulse-sensor-proxy
### References
- **Audit Report:** `docs/SECURITY_AUDIT_2025-11-07.md`
- **Audit Report:** `docs/SECURITY_AUDIT_2025-12-18.md`
- **Temperature Monitoring Overview:** `docs/security/TEMPERATURE_MONITORING.md`
- **Sensor Proxy Hardening:** `docs/security/SENSOR_PROXY_HARDENING.md`

View file

@ -2,7 +2,7 @@
Monitor real-time CPU and NVMe temperatures for your Proxmox nodes.
> **Deprecation notice (v5):** `pulse-sensor-proxy` is deprecated and not recommended for new deployments. Temperature monitoring should be done via the unified agent (`pulse-agent --enable-proxmox`). Existing proxy installs can continue during the migration window, but plan to migrate to the agent.
> **Deprecation notice (v5):** `pulse-sensor-proxy` is deprecated and not recommended for new deployments. Temperature monitoring should be done via the unified agent (`pulse-agent --enable-proxmox`). Existing proxy installs can continue during the migration window, but plan to migrate to the agent. In v5, legacy sensor-proxy endpoints are disabled by default unless `PULSE_ENABLE_SENSOR_PROXY=true` is set on the Pulse server.
## Recommended: Pulse Agent
@ -15,6 +15,23 @@ curl -fsSL http://<pulse-ip>:7655/install.sh | \
If you use the agent method, the rest of this document (sensor proxy) is optional. See `docs/security/TEMPERATURE_MONITORING.md` for the security model overview.
## Migration: pulse-sensor-proxy → pulse-agent
If you already deployed `pulse-sensor-proxy`, migrate to the agent to avoid proxy maintenance and remove SSH-from-container complexity:
1. Install `lm-sensors` on each Proxmox host (if not already): `apt install lm-sensors && sensors-detect`
2. Install the agent on each Proxmox host:
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-token> --enable-proxmox
```
3. Confirm temperatures are updating in the dashboard.
4. Disable the proxy service on hosts where it was installed:
```bash
sudo systemctl disable --now pulse-sensor-proxy
```
5. If your Pulse container had a proxy socket mount, remove the mount and remove `PULSE_SENSOR_PROXY_SOCKET` from the Pulse `.env` (for example `/data/.env` in Docker) before restarting Pulse.
## 🚀 Quick Start
### 1. Install the agent on Proxmox hosts

View file

@ -78,7 +78,11 @@ sudo pulse bootstrap-token
### Correlate Logs with Requests
Every API response has an `X-Request-ID` header. Use it to find the exact log entry:
```bash
grep "request_id=abc123" /var/log/pulse/pulse.log
# systemd / Proxmox LXC
journalctl -u pulse --no-pager | grep "request_id=abc123"
# Docker
docker logs pulse 2>&1 | grep "request_id=abc123"
```
### Check Permissions (Proxmox)

View file

@ -2,6 +2,8 @@
The unified agent (`pulse-agent`) combines host, Docker, and Kubernetes monitoring into a single binary. It replaces the separate `pulse-host-agent` and `pulse-docker-agent` for simpler deployment and management.
> Note: In v5, temperature monitoring should be done via `pulse-agent --enable-proxmox`. `pulse-sensor-proxy` is deprecated and retained only for existing installs during the migration window.
## Quick Start
Generate an installation command in the UI:

View file

@ -1,39 +0,0 @@
# 🧪 Mock Mode Development
Develop Pulse without real infrastructure using the mock data pipeline.
## 🚀 Quick Start
```bash
# Start dev stack
./scripts/hot-dev.sh
# Toggle mock mode
npm run mock:on # Enable
npm run mock:off # Disable
npm run mock:status # Check status
```
## ⚙️ Configuration
Edit `mock.env` (or `mock.env.local` for overrides):
| Variable | Default | Description |
| :--- | :--- | :--- |
| `PULSE_MOCK_MODE` | `false` | Enable mock mode. |
| `PULSE_MOCK_NODES` | `7` | Number of synthetic nodes. |
| `PULSE_MOCK_VMS_PER_NODE` | `5` | VMs per node. |
| `PULSE_MOCK_LXCS_PER_NODE` | `8` | Containers per node. |
| `PULSE_MOCK_RANDOM_METRICS` | `true` | Jitter metrics. |
| `PULSE_MOCK_STOPPED_PERCENT` | `20` | % of offline guests. |
| `PULSE_MOCK_TRENDS_SEED_DURATION` | `1h` | Pre-seed backend chart history (improves demo “Trends” immediately). |
| `PULSE_MOCK_TRENDS_SAMPLE_INTERVAL` | `30s` | Backend chart sampling interval while in mock mode. |
## How it Works
* **Data**: Swaps `PULSE_DATA_DIR` to `/opt/pulse/tmp/mock-data`.
* **Restart**: Backend restarts automatically; Frontend hot-reloads.
* **Reset**: To regenerate data, delete `/opt/pulse/tmp/mock-data` and toggle mock mode on.
## ⚠️ Limitations
* **Happy Path**: Focuses on standard flows; use real infrastructure for complex edge cases.
* **Webhooks**: Synthetic payloads only.
* **Encryption**: Uses local crypto stack (not a sandbox for auth).

View file

@ -5,6 +5,8 @@ This page describes the recommended v5 approach for temperature monitoring and t
For the full sensor-proxy setup guide (socket mounts, HTTP mode, troubleshooting), see:
`docs/TEMPERATURE_MONITORING.md`.
> **Deprecation notice (v5):** `pulse-sensor-proxy` is deprecated and not recommended for new deployments. Use `pulse-agent --enable-proxmox` for temperature monitoring. The sensor-proxy section below is retained for existing installations during the migration window. In v5, legacy sensor-proxy endpoints are disabled by default unless `PULSE_ENABLE_SENSOR_PROXY=true` is set on the Pulse server.
## Recommended: Pulse Agent
The simplest and most feature-rich method is installing the Pulse agent on your Proxmox nodes:

View file

@ -15,7 +15,6 @@ import { showSuccess, showError, showWarning } from '@/utils/toast';
import { logger } from '@/utils/logger';
import {
apiFetch,
apiFetchJSON,
clearApiToken as clearApiClientToken,
getApiToken as getApiClientToken,
setApiToken as setApiClientToken,
@ -980,9 +979,16 @@ const Settings: Component<SettingsProps> = (props) => {
const refreshHostProxyStatus = async (notify = false) => {
try {
const status = (await apiFetchJSON(
'/api/temperature-proxy/host-status',
)) as HostProxyStatusResponse;
const response = await apiFetch('/api/temperature-proxy/host-status');
if (response.status === 410) {
// pulse-sensor-proxy is deprecated and disabled by default in v5
setHostProxyStatus(null);
return;
}
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const status = (await response.json()) as HostProxyStatusResponse;
setHostProxyStatus(status);
if (notify) {
showSuccess('Host proxy status refreshed', undefined, 2000);

View file

@ -443,7 +443,11 @@ func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
socketHostState = r.monitor.SocketProxyHostDiagnostics()
}
diag.TemperatureProxy = buildTemperatureProxyDiagnostic(r.config, proxySync, socketHostState)
if r.config != nil && !r.config.EnableSensorProxy {
diag.TemperatureProxy = nil
} else {
diag.TemperatureProxy = buildTemperatureProxyDiagnostic(r.config, proxySync, socketHostState)
}
diag.APITokens = buildAPITokenDiagnostic(r.config, r.monitor)
// Test each configured node

View file

@ -203,11 +203,11 @@ func (r *Router) setupRoutes() {
r.mux.HandleFunc("/api/agents/host/report", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleReport)))
r.mux.HandleFunc("/api/agents/host/lookup", RequireAuth(r.config, RequireScope(config.ScopeHostReport, r.hostAgentHandlers.HandleLookup)))
r.mux.HandleFunc("/api/agents/host/", RequireAdmin(r.config, RequireScope(config.ScopeHostManage, r.hostAgentHandlers.HandleDeleteHost)))
r.mux.HandleFunc("/api/temperature-proxy/register", r.temperatureProxyHandlers.HandleRegister)
r.mux.HandleFunc("/api/temperature-proxy/authorized-nodes", r.temperatureProxyHandlers.HandleAuthorizedNodes)
r.mux.HandleFunc("/api/temperature-proxy/unregister", RequireAdmin(r.config, r.temperatureProxyHandlers.HandleUnregister))
r.mux.HandleFunc("/api/temperature-proxy/install-command", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleTemperatureProxyInstallCommand)))
r.mux.HandleFunc("/api/temperature-proxy/host-status", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.handleHostProxyStatus)))
r.mux.HandleFunc("/api/temperature-proxy/register", r.requireSensorProxyEnabled(r.temperatureProxyHandlers.HandleRegister))
r.mux.HandleFunc("/api/temperature-proxy/authorized-nodes", r.requireSensorProxyEnabled(r.temperatureProxyHandlers.HandleAuthorizedNodes))
r.mux.HandleFunc("/api/temperature-proxy/unregister", r.requireSensorProxyEnabled(RequireAdmin(r.config, r.temperatureProxyHandlers.HandleUnregister)))
r.mux.HandleFunc("/api/temperature-proxy/install-command", r.requireSensorProxyEnabled(RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleTemperatureProxyInstallCommand))))
r.mux.HandleFunc("/api/temperature-proxy/host-status", r.requireSensorProxyEnabled(RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.handleHostProxyStatus))))
r.mux.HandleFunc("/api/agents/docker/commands/", RequireAuth(r.config, RequireScope(config.ScopeDockerReport, r.dockerAgentHandlers.HandleCommandAck)))
r.mux.HandleFunc("/api/agents/docker/hosts/", RequireAdmin(r.config, RequireScope(config.ScopeDockerManage, r.dockerAgentHandlers.HandleDockerHostActions)))
r.mux.HandleFunc("/api/agents/kubernetes/clusters/", RequireAdmin(r.config, RequireScope(config.ScopeKubernetesManage, r.kubernetesAgentHandlers.HandleClusterActions)))
@ -218,12 +218,12 @@ func (r *Router) setupRoutes() {
r.mux.HandleFunc("/api/metrics-store/stats", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleMetricsStoreStats)))
r.mux.HandleFunc("/api/metrics-store/history", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, r.handleMetricsHistory)))
r.mux.HandleFunc("/api/diagnostics", RequireAuth(r.config, r.handleDiagnostics))
r.mux.HandleFunc("/api/diagnostics/temperature-proxy/register-nodes", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsRegisterProxyNodes)))
r.mux.HandleFunc("/api/diagnostics/temperature-proxy/register-nodes", r.requireSensorProxyEnabled(RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsRegisterProxyNodes))))
r.mux.HandleFunc("/api/diagnostics/docker/prepare-token", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.handleDiagnosticsDockerPrepareToken)))
r.mux.HandleFunc("/api/install/pulse-sensor-proxy", r.handleDownloadPulseSensorProxy)
r.mux.HandleFunc("/api/install/install-sensor-proxy.sh", r.handleDownloadInstallerScript)
r.mux.HandleFunc("/api/install/migrate-sensor-proxy-control-plane.sh", r.handleDownloadMigrationScript)
r.mux.HandleFunc("/api/install/migrate-temperature-proxy.sh", r.handleDownloadTemperatureProxyMigrationScript)
r.mux.HandleFunc("/api/install/pulse-sensor-proxy", r.requireSensorProxyEnabled(r.handleDownloadPulseSensorProxy))
r.mux.HandleFunc("/api/install/install-sensor-proxy.sh", r.requireSensorProxyEnabled(r.handleDownloadInstallerScript))
r.mux.HandleFunc("/api/install/migrate-sensor-proxy-control-plane.sh", r.requireSensorProxyEnabled(r.handleDownloadMigrationScript))
r.mux.HandleFunc("/api/install/migrate-temperature-proxy.sh", r.requireSensorProxyEnabled(r.handleDownloadTemperatureProxyMigrationScript))
r.mux.HandleFunc("/api/install/install-docker.sh", r.handleDownloadDockerInstallerScript)
r.mux.HandleFunc("/api/install/install.sh", r.handleDownloadUnifiedInstallScript)
r.mux.HandleFunc("/api/install/install.ps1", r.handleDownloadUnifiedInstallScriptPS)
@ -288,6 +288,30 @@ func (r *Router) setupRoutes() {
}
}))
// Docker host metadata routes (for managing Docker host custom URLs, e.g., Portainer links)
r.mux.HandleFunc("/api/docker/hosts/metadata", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, dockerMetadataHandler.HandleGetHostMetadata)))
r.mux.HandleFunc("/api/docker/hosts/metadata/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
if !ensureScope(w, req, config.ScopeMonitoringRead) {
return
}
dockerMetadataHandler.HandleGetHostMetadata(w, req)
case http.MethodPut, http.MethodPost:
if !ensureScope(w, req, config.ScopeMonitoringWrite) {
return
}
dockerMetadataHandler.HandleUpdateHostMetadata(w, req)
case http.MethodDelete:
if !ensureScope(w, req, config.ScopeMonitoringWrite) {
return
}
dockerMetadataHandler.HandleDeleteHostMetadata(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}))
// Host metadata routes
r.mux.HandleFunc("/api/hosts/metadata", RequireAuth(r.config, RequireScope(config.ScopeMonitoringRead, hostMetadataHandler.HandleGetMetadata)))
r.mux.HandleFunc("/api/hosts/metadata/", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
@ -1042,7 +1066,7 @@ func (r *Router) setupRoutes() {
r.mux.HandleFunc("/api/system/settings/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.systemSettingsHandler.HandleUpdateSystemSettings)))
r.mux.HandleFunc("/api/system/ssh-config", r.handleSSHConfig)
r.mux.HandleFunc("/api/system/verify-temperature-ssh", r.handleVerifyTemperatureSSH)
r.mux.HandleFunc("/api/system/proxy-public-key", r.handleProxyPublicKey)
r.mux.HandleFunc("/api/system/proxy-public-key", r.requireSensorProxyEnabled(r.handleProxyPublicKey))
// Old API token endpoints removed - now using /api/security/regenerate-token
// Agent execution server for AI tool use

View file

@ -0,0 +1,28 @@
package api
import "net/http"
func (r *Router) isSensorProxyEnabled() bool {
return r != nil && r.config != nil && r.config.EnableSensorProxy
}
func (r *Router) requireSensorProxyEnabled(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if r.isSensorProxyEnabled() {
next(w, req)
return
}
w.Header().Set("Warning", `299 - "pulse-sensor-proxy is deprecated and disabled by default in v5"`)
writeErrorResponse(
w,
http.StatusGone,
"sensor_proxy_disabled",
"pulse-sensor-proxy is deprecated and disabled by default in v5",
map[string]string{
"migration": "Use pulse-agent --enable-proxmox for temperature monitoring.",
"enable_env": "Set PULSE_ENABLE_SENSOR_PROXY=true (unsupported legacy) and restart Pulse to re-enable these endpoints.",
},
)
}
}

View file

@ -99,6 +99,7 @@ type Config struct {
BackupPollingInterval time.Duration `envconfig:"BACKUP_POLLING_INTERVAL"`
EnableBackupPolling bool `envconfig:"ENABLE_BACKUP_POLLING" default:"true"`
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
EnableSensorProxy bool `envconfig:"PULSE_ENABLE_SENSOR_PROXY" default:"false" json:"-"` // Legacy pulse-sensor-proxy support (deprecated, opt-in)
WebhookBatchDelay time.Duration `envconfig:"WEBHOOK_BATCH_DELAY" default:"10s"`
AdaptivePollingEnabled bool `envconfig:"ADAPTIVE_POLLING_ENABLED" default:"false"`
AdaptivePollingBaseInterval time.Duration `envconfig:"ADAPTIVE_POLLING_BASE_INTERVAL" default:"10s"`
@ -118,7 +119,6 @@ type Config struct {
MetricsRetentionHourlyDays int `json:"metricsRetentionHourlyDays"` // Hourly averages, default: 7 days
MetricsRetentionDailyDays int `json:"metricsRetentionDailyDays"` // Daily averages, default: 90 days
// Logging settings
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
LogFormat string `envconfig:"LOG_FORMAT" default:"auto"` // "json", "console", or "auto"
@ -580,6 +580,7 @@ func Load() (*Config, error) {
DiscoveryEnabled: false,
DiscoverySubnet: "auto",
TemperatureMonitoringEnabled: true,
EnableSensorProxy: false,
EnvOverrides: make(map[string]bool),
OIDC: NewOIDCConfig(),
// Metrics retention defaults (tiered)
@ -819,6 +820,22 @@ func Load() (*Config, error) {
}
}
if enabledStr := utils.GetenvTrim("PULSE_ENABLE_SENSOR_PROXY"); enabledStr != "" {
if enabled, err := strconv.ParseBool(enabledStr); err == nil {
cfg.EnableSensorProxy = enabled
cfg.EnvOverrides["PULSE_ENABLE_SENSOR_PROXY"] = true
if enabled {
log.Warn().Msg("Legacy pulse-sensor-proxy support enabled via PULSE_ENABLE_SENSOR_PROXY (deprecated, unsupported)")
} else {
log.Info().Msg("Legacy pulse-sensor-proxy support disabled via PULSE_ENABLE_SENSOR_PROXY")
}
} else {
log.Warn().
Str("value", enabledStr).
Msg("Invalid PULSE_ENABLE_SENSOR_PROXY value, ignoring")
}
}
if hideLocalLoginStr := utils.GetenvTrim("PULSE_AUTH_HIDE_LOCAL_LOGIN"); hideLocalLoginStr != "" {
if hide, err := strconv.ParseBool(hideLocalLoginStr); err == nil {
cfg.HideLocalLogin = hide

View file

@ -92,7 +92,7 @@ docker pull rcourtman/pulse:v${VERSION}
docker stop pulse && docker rm pulse
docker run -d --name pulse \\
--restart unless-stopped \\
-p 7655:7655 -p 7656:7656 \\
-p 7655:7655 \\
-v /opt/pulse/data:/data \\
rcourtman/pulse:v${VERSION}
\`\`\`
@ -322,7 +322,7 @@ docker pull rcourtman/pulse:v${VERSION}
docker stop pulse && docker rm pulse
docker run -d --name pulse \
--restart unless-stopped \
-p 7655:7655 -p 7656:7656 \
-p 7655:7655 \
-v /opt/pulse/data:/data \
rcourtman/pulse:v${VERSION}
```

View file

@ -15,9 +15,9 @@
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Usage:
# iwr -useb http://pulse-server:7656/install-host-agent.ps1 | iex
# iwr -useb http://pulse-server:7655/install-host-agent.ps1 | iex
# OR with parameters:
# $url = "http://pulse-server:7656"; $token = "your-token"; iwr -useb "$url/install-host-agent.ps1" | iex
# $url = "http://pulse-server:7655"; $token = "your-token"; iwr -useb "$url/install-host-agent.ps1" | iex
#
# Parameters can be passed via environment variables or script parameters
@ -248,7 +248,7 @@ if (-not $isAdmin) {
# Interactive prompts if parameters not provided
if (-not $PulseUrl) {
$PulseUrl = Read-Host "Enter Pulse server URL (e.g., http://pulse.example.com:7656)"
$PulseUrl = Read-Host "Enter Pulse server URL (e.g., http://pulse.example.com:7655)"
}
$PulseUrl = $PulseUrl.TrimEnd('/')

View file

@ -1,7 +1,7 @@
# Pulse Host Agent Uninstallation Script for Windows
#
# Usage:
# iwr -useb http://pulse-server:7656/uninstall-host-agent.ps1 | iex
# iwr -useb http://pulse-server:7655/uninstall-host-agent.ps1 | iex
#
param(