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()