mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
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:
+59
-16
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user