Integrate reviewed bounded security guidance

Accept the exact Product Intelligence security documentation and regression-test commit. Preserve existing logging deployments and private evidence; source guidance is not installed runtime recovery.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-11 23:55:38 +01:00
3 changed files with 237 additions and 104 deletions
+84 -52
View File
@@ -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:** <https://github.com/rcourtman/pulse/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
+84 -52
View File
@@ -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:** <https://github.com/rcourtman/pulse/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
+69
View File
@@ -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()