State credential encryption and log-disclosure limits accurately

The security guide implied all credentials and outputs were safe despite the local key, external password sources and private diagnostic records. Explain the active data path, matching-key recovery and review-before-sharing boundaries; keep existing administration recipes unchanged.

Contract-Neutral: Documentation and document regression checks only; no storage, encryption, authentication, logging or runtime contract delta.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-09 15:59:57 +01:00
parent 95e1217c17
commit c3ec820087
3 changed files with 166 additions and 32 deletions
+59 -16
View File
@@ -175,12 +175,12 @@ Pulse includes a non-intrusive security warning system that helps you
understand your security posture.
### Security Score
Your instance receives a score from 0‑5 based on:
- ✅ Credentials encrypted at rest (always enabled)
- ✅ Export/import protection
- ⚠️ Authentication enabled
- ⚠️ HTTPS connection
- ⚠️ Audit logging
**Settings → Security → Overview** summarises authentication methods,
API-token configuration, export protection, HTTPS and audit logging. These
indicators describe configuration checks, not a complete security audit. A
strong score does not establish that every file is encrypted, a backup is safe
to share, or stored credentials can be recovered without their encryption key.
### Dismissing Warnings
If you're comfortable with your security setup, you can dismiss warnings:
@@ -191,21 +191,61 @@ If you're comfortable with your security setup, you can dismiss warnings:
## Credential Security
### Encrypted at Rest (AES-256-GCM)
- **Node credentials**: passwords and API tokens (`/etc/pulse/nodes.enc`)
- **Email settings**: SMTP passwords (`/etc/pulse/email.enc`)
- **Webhook data**: URLs and auth headers (`/etc/pulse/webhooks.enc`)
- **Encryption key**: auto-generated (`/etc/pulse/.encryption.key`)
Pulse encrypts configuration saved through its credential store. Examples in
the **active data directory** include:
| Stored configuration | File |
| --- | --- |
| Proxmox VE, PBS and PMG connection credentials | `nodes.enc` |
| SMTP settings, including passwords | `email.enc` |
| Webhook URLs and authentication headers | `webhooks.enc` |
| TrueNAS and vSphere connection credentials | `truenas.enc`, `vmware.enc` |
| AI provider credentials and SSO configuration | `ai.enc`, `sso.enc` |
The directory is normally `/etc/pulse` for systemd or `/data` inside Docker;
`PULSE_DATA_DIR` and deployment configuration can select another location.
Use the existing deployment's actual data path, not an assumed default.
### Encryption scope and recovery
The auto-generated **`.encryption.key`** is stored alongside these files. It
is key material, not an encrypted credential file. Anyone with the encrypted
files **and their matching key** can decrypt them. Protect the entire data
directory, mounted volume and recovery copies; encryption does not protect
against an account or process that can read both. Pulse protects its key file
with owner-only permissions (`0600`, with a `0700` parent directory); preserve
the intended service ownership rather than granting wider access to fix a read
failure.
This is not whole-directory or whole-disk encryption. Deployment environment
files, service definitions, agent token files, logs and history can still
contain secrets or identifying details. Quick Security Setup saves a bcrypt
password hash, but hashing an externally supplied plaintext `PULSE_AUTH_PASS`
for use does not rewrite its original deployment source. Keep hashes and raw
client credentials private too; see [private authentication configuration](docs/CONFIGURATION.md#private-docker-authentication-file).
Keep encrypted files and their matching key together in a **private recovery
backup**, not in a public attachment. Do not delete or regenerate the key, mix
files from different instances, or reset the data directory to repair a
decryption error. Preserve the existing files and reconcile the data path and
matching backup first. A file copy alone is not verified recovery; follow the
[server migration and restore guidance](docs/MIGRATION.md).
### Security Features
- **Logs**: token values masked with `***` in all outputs
- **Logs**: targeted masking and redaction are not a guarantee that every
output is safe to share. Free-text errors and historical records can contain
secrets or private details. Review excerpts locally; follow
[safe diagnostics collection](docs/TROUBLESHOOTING.md#collect-diagnostics-safely)
and the [log-sharing precautions](docs/TROUBLESHOOTING.md#inspect-notification-logs).
- **API**: ordinary configuration reads redact stored credentials; newly issued
tokens are deliberately revealed for their owner to save securely
- **Export**: requires authentication (session, proxy auth, or `X-API-Token`
header) to extract credentials
- **Migration**: use passphrase-protected export/import (see
[Migration Guide](docs/MIGRATION.md))
- **Auto-migration**: unencrypted configs automatically migrate to encrypted
format
- **Auto-migration**: supported legacy credential files migrate when loaded;
this does not scrub earlier backups or external deployment configuration
## Export/Import Protection
@@ -244,7 +284,8 @@ for sensitive data.
## Security Features Summary
### Core Protection
- **Encryption**: credentials encrypted at rest (AES-256-GCM)
- **Encryption**: credential-store files use AES-256-GCM, within the
[scope and recovery limits](#encryption-scope-and-recovery) above
- **Export protection**: exports always encrypted with a passphrase
- **Minimum passphrase**: 12 characters required for exports
- **Security tab**: check status in *Settings → Security → Overview*
@@ -252,14 +293,16 @@ for sensitive data.
### Advanced Security (When Authentication Enabled)
- **Password security**
- Bcrypt hashing with cost factor 12 (60‑character hash)
- Passwords never stored in plain text
- Quick Security Setup persists the hash, not the password; external
deployment sources remain separately managed
- Automatic hashing during security setup
- **Critical**: bcrypt hashes must be exactly 60 characters
- **API token security**
- 64‑character hex tokens (32 bytes entropy)
- SHA3-256 hashed before storage (64‑character hash)
- Raw token shown only once
- Tokens never stored in plain text
- Pulse-issued token records store verification hashes; raw client and agent
token copies must remain private
- Stored in `api_tokens.json` and managed via the UI
- API-only mode supported (no password auth required)
- **CSRF protection**: session-authenticated state changes require CSRF tokens;
+59 -16
View File
@@ -175,12 +175,12 @@ Pulse includes a non-intrusive security warning system that helps you
understand your security posture.
### Security Score
Your instance receives a score from 0‑5 based on:
- ✅ Credentials encrypted at rest (always enabled)
- ✅ Export/import protection
- ⚠️ Authentication enabled
- ⚠️ HTTPS connection
- ⚠️ Audit logging
**Settings → Security → Overview** summarises authentication methods,
API-token configuration, export protection, HTTPS and audit logging. These
indicators describe configuration checks, not a complete security audit. A
strong score does not establish that every file is encrypted, a backup is safe
to share, or stored credentials can be recovered without their encryption key.
### Dismissing Warnings
If you're comfortable with your security setup, you can dismiss warnings:
@@ -191,21 +191,61 @@ If you're comfortable with your security setup, you can dismiss warnings:
## Credential Security
### Encrypted at Rest (AES-256-GCM)
- **Node credentials**: passwords and API tokens (`/etc/pulse/nodes.enc`)
- **Email settings**: SMTP passwords (`/etc/pulse/email.enc`)
- **Webhook data**: URLs and auth headers (`/etc/pulse/webhooks.enc`)
- **Encryption key**: auto-generated (`/etc/pulse/.encryption.key`)
Pulse encrypts configuration saved through its credential store. Examples in
the **active data directory** include:
| Stored configuration | File |
| --- | --- |
| Proxmox VE, PBS and PMG connection credentials | `nodes.enc` |
| SMTP settings, including passwords | `email.enc` |
| Webhook URLs and authentication headers | `webhooks.enc` |
| TrueNAS and vSphere connection credentials | `truenas.enc`, `vmware.enc` |
| AI provider credentials and SSO configuration | `ai.enc`, `sso.enc` |
The directory is normally `/etc/pulse` for systemd or `/data` inside Docker;
`PULSE_DATA_DIR` and deployment configuration can select another location.
Use the existing deployment's actual data path, not an assumed default.
### Encryption scope and recovery
The auto-generated **`.encryption.key`** is stored alongside these files. It
is key material, not an encrypted credential file. Anyone with the encrypted
files **and their matching key** can decrypt them. Protect the entire data
directory, mounted volume and recovery copies; encryption does not protect
against an account or process that can read both. Pulse protects its key file
with owner-only permissions (`0600`, with a `0700` parent directory); preserve
the intended service ownership rather than granting wider access to fix a read
failure.
This is not whole-directory or whole-disk encryption. Deployment environment
files, service definitions, agent token files, logs and history can still
contain secrets or identifying details. Quick Security Setup saves a bcrypt
password hash, but hashing an externally supplied plaintext `PULSE_AUTH_PASS`
for use does not rewrite its original deployment source. Keep hashes and raw
client credentials private too; see [private authentication configuration](docs/CONFIGURATION.md#private-docker-authentication-file).
Keep encrypted files and their matching key together in a **private recovery
backup**, not in a public attachment. Do not delete or regenerate the key, mix
files from different instances, or reset the data directory to repair a
decryption error. Preserve the existing files and reconcile the data path and
matching backup first. A file copy alone is not verified recovery; follow the
[server migration and restore guidance](docs/MIGRATION.md).
### Security Features
- **Logs**: token values masked with `***` in all outputs
- **Logs**: targeted masking and redaction are not a guarantee that every
output is safe to share. Free-text errors and historical records can contain
secrets or private details. Review excerpts locally; follow
[safe diagnostics collection](docs/TROUBLESHOOTING.md#collect-diagnostics-safely)
and the [log-sharing precautions](docs/TROUBLESHOOTING.md#inspect-notification-logs).
- **API**: ordinary configuration reads redact stored credentials; newly issued
tokens are deliberately revealed for their owner to save securely
- **Export**: requires authentication (session, proxy auth, or `X-API-Token`
header) to extract credentials
- **Migration**: use passphrase-protected export/import (see
[Migration Guide](docs/MIGRATION.md))
- **Auto-migration**: unencrypted configs automatically migrate to encrypted
format
- **Auto-migration**: supported legacy credential files migrate when loaded;
this does not scrub earlier backups or external deployment configuration
## Export/Import Protection
@@ -244,7 +284,8 @@ for sensitive data.
## Security Features Summary
### Core Protection
- **Encryption**: credentials encrypted at rest (AES-256-GCM)
- **Encryption**: credential-store files use AES-256-GCM, within the
[scope and recovery limits](#encryption-scope-and-recovery) above
- **Export protection**: exports always encrypted with a passphrase
- **Minimum passphrase**: 12 characters required for exports
- **Security tab**: check status in *Settings → Security → Overview*
@@ -252,14 +293,16 @@ for sensitive data.
### Advanced Security (When Authentication Enabled)
- **Password security**
- Bcrypt hashing with cost factor 12 (60‑character hash)
- Passwords never stored in plain text
- Quick Security Setup persists the hash, not the password; external
deployment sources remain separately managed
- Automatic hashing during security setup
- **Critical**: bcrypt hashes must be exactly 60 characters
- **API token security**
- 64‑character hex tokens (32 bytes entropy)
- SHA3-256 hashed before storage (64‑character hash)
- Raw token shown only once
- Tokens never stored in plain text
- Pulse-issued token records store verification hashes; raw client and agent
token copies must remain private
- Stored in `api_tokens.json` and managed via the UI
- API-only mode supported (no password auth required)
- **CSRF protection**: session-authenticated state changes require CSRF tokens;
+48
View File
@@ -156,6 +156,54 @@ class SecurityDocsTest(unittest.TestCase):
self.assertIn("docs/TROUBLESHOOTING.md#i-forgot-my-password", guide)
self.assertNotIn("### What's **Not** Encrypted", guide)
def test_encrypted_file_examples_match_the_actual_store_and_data_path(self):
guide = " ".join(DOC.read_text().split())
persistence = (ROOT / "internal/config/persistence.go").read_text()
crypto = (ROOT / "internal/crypto/crypto.go").read_text()
for filename in ("nodes.enc", "email.enc", "webhooks.enc", "truenas.enc",
"vmware.enc", "ai.enc", "sso.enc"):
with self.subTest(filename=filename):
self.assertIn(f'resolveLeaf("{filename}")', persistence)
self.assertIn(f"`{filename}`", guide)
self.assertRegex(crypto, r'encryptionKeyFileName\s*=\s*"\.encryption\.key"')
for boundary in ("active data directory", "`PULSE_DATA_DIR`", "`/etc/pulse`",
"`/data`", "existing deployment's actual data path"):
self.assertIn(boundary, guide)
self.assertNotIn("/etc/pulse/nodes.enc", guide)
def test_key_scope_and_recovery_never_imply_safe_public_or_unmatched_copies(self):
guide = " ".join(DOC.read_text().split())
for boundary in ("stored alongside", "not an encrypted credential file",
"matching key", "can decrypt", "entire data directory",
"private recovery backup", "not in a public attachment",
"owner-only", "0600", "0700", "service ownership",
"Do not delete or regenerate the key", "mix files from different instances",
"Preserve the existing files", "not verified recovery",
"docs/MIGRATION.md"):
self.assertIn(boundary, guide)
def test_deployment_credentials_and_unencrypted_evidence_remain_private(self):
guide = " ".join(DOC.read_text().split())
for boundary in ("not whole-directory or whole-disk encryption",
"environment files", "service definitions", "agent token files",
"logs and history", "bcrypt", "plaintext `PULSE_AUTH_PASS`",
"does not rewrite", "original deployment source",
"hashes and raw client credentials private",
"does not scrub earlier backups", "external deployment configuration"):
self.assertIn(boundary, guide)
for overclaim in ("Passwords never stored in plain text", "Tokens never stored in plain text",
"Credentials encrypted at rest (always enabled)"):
self.assertNotIn(overclaim, guide)
def test_security_score_and_log_masking_are_not_disclosure_guarantees(self):
guide = " ".join(DOC.read_text().split())
for boundary in ("not a complete security audit", "every file is encrypted",
"not a guarantee", "Free-text errors and historical records",
"Review excerpts locally", "#collect-diagnostics-safely",
"#inspect-notification-logs"):
self.assertIn(boundary, guide)
self.assertNotIn("token values masked with `***` in all outputs", guide)
if __name__ == "__main__":
unittest.main()