From 10c8818912ec8c7d0a0e78e567f0b904bad52d15 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:11:34 +0100 Subject: [PATCH] Keep security verification bounded and preserve logging state The canonical security guide still encouraged Debug logging, broad service/log inspection and a fresh unmounted Docker container. Route verification through existing bounded private evidence, distinguish file rotation from journal, container and audit retention, and preserve the active deployment when changing logging. Keep the shipped guide synchronized and check these boundaries against the current server source. Contract-Neutral: Correct existing security guidance and its regression checks without changing runtime, permissions, credential handling, API behaviour or subsystem contracts. Change-source: pulse-maintainer --- SECURITY.md | 136 +++++++++++++++--------- frontend-modern/public/docs/SECURITY.md | 136 +++++++++++++++--------- scripts/tests/test_security_docs.py | 69 ++++++++++++ 3 files changed, 237 insertions(+), 104 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 739b72da95..3776906408 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -105,19 +105,17 @@ rotate keys regularly. #### Auditing Your Deployment -```bash -# Detect vulnerable containers -ls /home/pulse/.ssh/id_ed25519* 2>/dev/null && echo "⚠️ SSH keys present" -``` +Review the existing deployment's SSH keys and mounts privately with its +administrator. Checking one filename, or a failed file lookup, does not prove +that no key is present. Do not share private keys, full service definitions or +container inspections; those can contain credentials. -Verify temperature collection is agent-based: - -- UI: the Proxmox or Machines page shows the host as agent-backed after the agent reports. -- On each Proxmox host: - ```bash - systemctl status pulse-agent - journalctl -u pulse-agent -n 200 --no-pager - ``` +For temperature collection, check the affected host on **Proxmox** or +**Machines**, keeping its agent version, last activity and sensor observation +time separate. A reporting agent does not prove that every reading is fresh. +Use the [existing Linux temperature checks](docs/TEMPERATURE_MONITORING.md#check-existing-linux-readings-safely) +only when collection is safe. During a backup, freeze/thaw or an unresponsive-host +incident, defer live checks and follow [backup safety](docs/VM_DISK_MONITORING.md#backup-safety). **Documentation:** [Critical security notice for container deployments](#critical-security-notice-for-container-deployments) **Issues:** @@ -544,42 +542,57 @@ Endpoints enforce scope checks before processing. A token without the required s ### Runtime Logging Configuration -Pulse supports configurable logging (level, format, optional file output, rotation) via environment variables. +Start with existing evidence at the current log level and the +[bounded Pulse log readers](docs/TROUBLESHOOTING.md#inspect-notification-logs). +**Debug is not a prerequisite for a report.** It can produce much more output, +including private names, paths and raw guest-filesystem responses. Do not enable +it, repeat the failed action or run Diagnostics just to fill an evidence gap. +If the interface stalls after enabling Debug, return to the previous working +level through the existing deployment's log-level control; do not reproduce the +stall. A responsive interface afterwards is not proof of underlying recovery. -#### 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 +The **Log Level** control in **Settings → Support → System Logs** changes the +server-wide threshold immediately, not just the browser's filter. Its saved +value is used at startup unless `LOG_LEVEL` overrides it. See +[log levels and display controls](docs/CONFIGURATION.md#log-levels). -#### Configuration Options +#### Server logging environment settings -**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 +| Variable | Purpose | Default | +| --- | --- | --- | +| `LOG_LEVEL` | Startup threshold (`info`, `warn`, `error` or `debug`) | `info` | +| `LOG_FORMAT` | Output format (`auto`, `json` or `console`) | `auto` | +| `LOG_FILE` | Additional rotating file output | Unset | +| `LOG_MAX_SIZE` | File rotation size in MB | `100` | +| `LOG_MAX_AGE` | Rotated-file retention in positive days | `30` | +| `LOG_COMPRESS` | Compress rotated files | `true` | -# 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 -``` +File rotation and compression apply only when `LOG_FILE` is configured. These +settings do not bound the systemd journal, container logs or browser log buffer, +and do not configure the separate **Audit Log** retention. Zero or negative +`LOG_MAX_AGE` values do not disable cleanup: they are ignored with a startup +warning and the default applies. JSON formatting and compression do not redact +private information. Preserve incident evidence; do not delete logs or shorten +retention merely to complete a report. -**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 +Change only the intended settings in the existing deployment's managed source: + +- **Systemd / Proxmox LXC:** edit the active service's environment file or + drop-in privately. Reload the service manager if its unit or drop-in changed, + then restart that service during a suitable maintenance window. For LXC, + work inside the Pulse container, not on the Proxmox host. +- **Docker / Compose:** use the existing manager's recreate/redeploy operation + for an environment change; `docker restart` does not apply it. Preserve the + same image, persistent data mounts, credentials and other settings. Do not + start a replacement `docker run` container just to change logging. + +**Pause Stream** and **Clear Log Output** affect only the browser display; they +do not stop server logging, erase retained logs or remove them from a +**Support Bundle**. The bundle can include a complete log file, configuration +and environment information. Keep it private and share only relevant, locally +reviewed, redacted excerpts, not the whole archive. During backups, freeze/thaw +or an unresponsive-host incident, defer collection and follow +[safe diagnostics guidance](docs/TROUBLESHOOTING.md#collect-diagnostics-safely). ## CORS (Cross-Origin Resource Sharing) @@ -646,7 +659,12 @@ straight to `jq` can make a failed request look successful. - **Backoff Delays**: Increased backoff may indicate rate limiting or errors - **Error Rates**: Track failed API calls and authentication attempts -Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting. +Use existing status and incident evidence first. The endpoint above is a +read-only status check, not proof that every workload or collector is healthy. +Collect new evidence only when Pulse is running and collection is safe; follow +[safe diagnostics guidance](docs/TROUBLESHOOTING.md#collect-diagnostics-safely) +rather than running Diagnostics during backups, freeze/thaw or an +unresponsive-host incident. Review responses locally before sharing them. ### Existing Mobile Pairings (Retirement) @@ -690,14 +708,28 @@ open Pulse in the phone's browser. - ❌ **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$`) -- Server-side API token records stored as SHA3-256 hashes (64 characters); - agents still need a private raw credential to authenticate -- Secure file permissions on `/etc/pulse/.env` (600) -- No credential leaks in API responses (test with `curl`) + +Review the existing deployment privately; a successful check is not a complete +security audit, and absence from one excerpt is not proof that no secret leaked. + +- Protect the active credential source and its backups, including any + deployment-supplied plaintext password. Follow + [private authentication configuration](docs/CONFIGURATION.md#private-docker-authentication-file), + rather than posting environment files or replacing a working installation. +- Server-side API token records contain SHA3-256 hashes; agents still require + a private raw credential. Neither a token's shape nor a partial hint match + proves that it is the credential used by a particular running process. +- Preserve the intended service ownership and owner-only access to sensitive + files in the actual data directory. Review the + [encryption and recovery limits](#encryption-scope-and-recovery); do not grant + wider permissions or delete keys to repair a read failure. +- Review relevant existing logs, API responses and screenshots locally before + sharing a redacted excerpt. Use the [bounded log readers](docs/TROUBLESHOOTING.md#inspect-notification-logs), + not full logs, Support Bundles, service environments or network exports. + Do not extract browser cookies or use **Copy as cURL** to create evidence. +- If private information was posted, use + [disclosure containment](docs/TROUBLESHOOTING.md#if-you-already-posted-private-information). + Removing the public copy does not revoke an exposed credential. ## Account Lockout and Recovery diff --git a/frontend-modern/public/docs/SECURITY.md b/frontend-modern/public/docs/SECURITY.md index 739b72da95..3776906408 100644 --- a/frontend-modern/public/docs/SECURITY.md +++ b/frontend-modern/public/docs/SECURITY.md @@ -105,19 +105,17 @@ rotate keys regularly. #### Auditing Your Deployment -```bash -# Detect vulnerable containers -ls /home/pulse/.ssh/id_ed25519* 2>/dev/null && echo "⚠️ SSH keys present" -``` +Review the existing deployment's SSH keys and mounts privately with its +administrator. Checking one filename, or a failed file lookup, does not prove +that no key is present. Do not share private keys, full service definitions or +container inspections; those can contain credentials. -Verify temperature collection is agent-based: - -- UI: the Proxmox or Machines page shows the host as agent-backed after the agent reports. -- On each Proxmox host: - ```bash - systemctl status pulse-agent - journalctl -u pulse-agent -n 200 --no-pager - ``` +For temperature collection, check the affected host on **Proxmox** or +**Machines**, keeping its agent version, last activity and sensor observation +time separate. A reporting agent does not prove that every reading is fresh. +Use the [existing Linux temperature checks](docs/TEMPERATURE_MONITORING.md#check-existing-linux-readings-safely) +only when collection is safe. During a backup, freeze/thaw or an unresponsive-host +incident, defer live checks and follow [backup safety](docs/VM_DISK_MONITORING.md#backup-safety). **Documentation:** [Critical security notice for container deployments](#critical-security-notice-for-container-deployments) **Issues:** @@ -544,42 +542,57 @@ Endpoints enforce scope checks before processing. A token without the required s ### Runtime Logging Configuration -Pulse supports configurable logging (level, format, optional file output, rotation) via environment variables. +Start with existing evidence at the current log level and the +[bounded Pulse log readers](docs/TROUBLESHOOTING.md#inspect-notification-logs). +**Debug is not a prerequisite for a report.** It can produce much more output, +including private names, paths and raw guest-filesystem responses. Do not enable +it, repeat the failed action or run Diagnostics just to fill an evidence gap. +If the interface stalls after enabling Debug, return to the previous working +level through the existing deployment's log-level control; do not reproduce the +stall. A responsive interface afterwards is not proof of underlying recovery. -#### 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 +The **Log Level** control in **Settings → Support → System Logs** changes the +server-wide threshold immediately, not just the browser's filter. Its saved +value is used at startup unless `LOG_LEVEL` overrides it. See +[log levels and display controls](docs/CONFIGURATION.md#log-levels). -#### Configuration Options +#### Server logging environment settings -**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 +| Variable | Purpose | Default | +| --- | --- | --- | +| `LOG_LEVEL` | Startup threshold (`info`, `warn`, `error` or `debug`) | `info` | +| `LOG_FORMAT` | Output format (`auto`, `json` or `console`) | `auto` | +| `LOG_FILE` | Additional rotating file output | Unset | +| `LOG_MAX_SIZE` | File rotation size in MB | `100` | +| `LOG_MAX_AGE` | Rotated-file retention in positive days | `30` | +| `LOG_COMPRESS` | Compress rotated files | `true` | -# 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 -``` +File rotation and compression apply only when `LOG_FILE` is configured. These +settings do not bound the systemd journal, container logs or browser log buffer, +and do not configure the separate **Audit Log** retention. Zero or negative +`LOG_MAX_AGE` values do not disable cleanup: they are ignored with a startup +warning and the default applies. JSON formatting and compression do not redact +private information. Preserve incident evidence; do not delete logs or shorten +retention merely to complete a report. -**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 +Change only the intended settings in the existing deployment's managed source: + +- **Systemd / Proxmox LXC:** edit the active service's environment file or + drop-in privately. Reload the service manager if its unit or drop-in changed, + then restart that service during a suitable maintenance window. For LXC, + work inside the Pulse container, not on the Proxmox host. +- **Docker / Compose:** use the existing manager's recreate/redeploy operation + for an environment change; `docker restart` does not apply it. Preserve the + same image, persistent data mounts, credentials and other settings. Do not + start a replacement `docker run` container just to change logging. + +**Pause Stream** and **Clear Log Output** affect only the browser display; they +do not stop server logging, erase retained logs or remove them from a +**Support Bundle**. The bundle can include a complete log file, configuration +and environment information. Keep it private and share only relevant, locally +reviewed, redacted excerpts, not the whole archive. During backups, freeze/thaw +or an unresponsive-host incident, defer collection and follow +[safe diagnostics guidance](docs/TROUBLESHOOTING.md#collect-diagnostics-safely). ## CORS (Cross-Origin Resource Sharing) @@ -646,7 +659,12 @@ straight to `jq` can make a failed request look successful. - **Backoff Delays**: Increased backoff may indicate rate limiting or errors - **Error Rates**: Track failed API calls and authentication attempts -Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting. +Use existing status and incident evidence first. The endpoint above is a +read-only status check, not proof that every workload or collector is healthy. +Collect new evidence only when Pulse is running and collection is safe; follow +[safe diagnostics guidance](docs/TROUBLESHOOTING.md#collect-diagnostics-safely) +rather than running Diagnostics during backups, freeze/thaw or an +unresponsive-host incident. Review responses locally before sharing them. ### Existing Mobile Pairings (Retirement) @@ -690,14 +708,28 @@ open Pulse in the phone's browser. - ❌ **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$`) -- Server-side API token records stored as SHA3-256 hashes (64 characters); - agents still need a private raw credential to authenticate -- Secure file permissions on `/etc/pulse/.env` (600) -- No credential leaks in API responses (test with `curl`) + +Review the existing deployment privately; a successful check is not a complete +security audit, and absence from one excerpt is not proof that no secret leaked. + +- Protect the active credential source and its backups, including any + deployment-supplied plaintext password. Follow + [private authentication configuration](docs/CONFIGURATION.md#private-docker-authentication-file), + rather than posting environment files or replacing a working installation. +- Server-side API token records contain SHA3-256 hashes; agents still require + a private raw credential. Neither a token's shape nor a partial hint match + proves that it is the credential used by a particular running process. +- Preserve the intended service ownership and owner-only access to sensitive + files in the actual data directory. Review the + [encryption and recovery limits](#encryption-scope-and-recovery); do not grant + wider permissions or delete keys to repair a read failure. +- Review relevant existing logs, API responses and screenshots locally before + sharing a redacted excerpt. Use the [bounded log readers](docs/TROUBLESHOOTING.md#inspect-notification-logs), + not full logs, Support Bundles, service environments or network exports. + Do not extract browser cookies or use **Copy as cURL** to create evidence. +- If private information was posted, use + [disclosure containment](docs/TROUBLESHOOTING.md#if-you-already-posted-private-information). + Removing the public copy does not revoke an exposed credential. ## Account Lockout and Recovery diff --git a/scripts/tests/test_security_docs.py b/scripts/tests/test_security_docs.py index 1ac05485ed..99447d0006 100644 --- a/scripts/tests/test_security_docs.py +++ b/scripts/tests/test_security_docs.py @@ -204,6 +204,75 @@ class SecurityDocsTest(unittest.TestCase): self.assertIn(boundary, guide) self.assertNotIn("token values masked with `***` in all outputs", guide) + def test_security_checks_use_existing_bounded_evidence_not_broad_log_recipes(self): + source = DOC.read_text() + audit = source.split("#### Auditing Your Deployment\n", 1)[1].split("\n---\n", 1)[0] + checklist = source.split("### Verification Checklist\n", 1)[1].split("\n## ", 1)[0] + self.assertNotRegex(audit + checklist, + r"(?:systemctl status|journalctl|docker logs|ls /home/|```bash)") + for link in ("docs/TEMPERATURE_MONITORING.md#check-existing-linux-readings-safely", + "docs/VM_DISK_MONITORING.md#backup-safety"): + self.assertIn(link, audit) + for link in ("docs/TROUBLESHOOTING.md#inspect-notification-logs", + "docs/TROUBLESHOOTING.md#if-you-already-posted-private-information"): + self.assertIn(link, checklist) + guide = " ".join((audit + checklist).split()) + for boundary in ("does not prove", "does not revoke", "full service definitions", + "container inspections", "Copy as cURL", "partial hint match"): + self.assertIn(boundary, guide) + + def test_logging_defaults_and_file_only_rotation_match_server_source(self): + source = DOC.read_text() + logging = source.split("### Runtime Logging Configuration\n", 1)[1].split("\n## ", 1)[0] + rows = dict(re.findall(r"^\| `(LOG_[A-Z_]+)` \| [^|]+ \| (.*?) \|$", + logging, re.MULTILINE)) + config = (ROOT / "internal/config/config.go").read_text() + defaults = dict(re.findall(r'envconfig:"(LOG_[A-Z_]+)" default:"([^"]*)"', config)) + self.assertEqual(set(rows), set(defaults)) + self.assertEqual({key: ("Unset" if not value else f"`{value}`") + for key, value in defaults.items()}, rows) + writer = (ROOT / "internal/logging/logging.go").read_text() + self.assertRegex(writer, r'func newRollingFileWriter\(cfg Config\).*?\{\s*' + r'path := strings.TrimSpace\(cfg.FilePath\)\s*' + r'if path == "" \{\s*return nil, nil') + guide = " ".join(logging.split()) + for boundary in ("only when `LOG_FILE` is configured", "systemd journal", + "container logs", "browser log buffer", "separate **Audit Log**", + "Zero or negative", "do not disable cleanup", + "do not redact", "do not delete logs or shorten retention"): + self.assertIn(boundary, guide) + self.assertNotIn("manage audit log retention", guide) + + def test_logging_changes_preserve_the_existing_deployment_and_runtime_distinction(self): + logging = DOC.read_text().split("### Runtime Logging Configuration\n", 1)[1].split("\n## ", 1)[0] + # Environment-file settings are not a new-install command or a shell + # script containing a systemd [Service] stanza. + self.assertNotIn("```bash", logging) + guide = " ".join(logging.split()) + for boundary in ("server-wide threshold immediately", "at startup unless `LOG_LEVEL`", + "docs/CONFIGURATION.md#log-levels", "environment file or drop-in", + "inside the Pulse container", "recreate/redeploy", + "`docker restart` does not apply it", "same image", + "persistent data mounts", "credentials and other settings"): + self.assertIn(boundary, guide) + + def test_debug_display_and_support_bundle_guidance_does_not_invite_replay_or_disclosure(self): + source = DOC.read_text() + logging = source.split("### Runtime Logging Configuration\n", 1)[1].split("\n## ", 1)[0] + observability = source.split("### Scheduler Health API\n", 1)[1].split("\n### ", 1)[0] + self.assertNotIn("Enable debug logging temporarily", source) + guide = " ".join(logging.split()) + for boundary in ("Debug is not a prerequisite", "repeat the failed action", + "previous working level", "not proof of underlying recovery", + "**Pause Stream**", "**Clear Log Output**", "only the browser display", + "complete log file", "configuration and environment information", + "Keep it private", "locally reviewed, redacted excerpts", + "backups, freeze/thaw", "unresponsive-host incident"): + self.assertIn(boundary, guide) + for section in (logging, observability): + self.assertIn("docs/TROUBLESHOOTING.md#collect-diagnostics-safely", section) + self.assertIn("not proof that every workload or collector is healthy", observability) + if __name__ == "__main__": unittest.main()