mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-02 20:29:43 +00:00
Integrate reviewed audit recovery guidance repair
Preserve the exact reviewed specialist commit and its documentation and signer controls alongside the current canonical frontier. Change-source: pulse-maintainer
This commit is contained in:
commit
db35048aca
10 changed files with 448 additions and 36 deletions
|
|
@ -4,6 +4,11 @@ Pulse's audit log records security-relevant events with tamper-evident signature
|
|||
|
||||
**Requires:** Pro, legacy Pro+, or Cloud license with the `audit_logging` capability to query, export, and verify events via the API. Events are recorded on all plans, but the API endpoints are license-gated.
|
||||
|
||||
An active licence alone does not enable the audit UI/API on the public
|
||||
community runtime. If the panel says **Pulse Pro runtime required**, follow
|
||||
**Download Pulse Pro** to the private runtime for your deployment. Do not buy
|
||||
a second licence or reset the data directory to clear this gate.
|
||||
|
||||
For plan details, see [PULSE_PRO.md](PULSE_PRO.md). For API endpoints, see [API Reference](API.md#-audit-log-pro).
|
||||
|
||||
---
|
||||
|
|
@ -116,7 +121,9 @@ sensitive data; keep it outside shared repositories and issue attachments.
|
|||
|
||||
## Tamper Detection
|
||||
|
||||
Every audit event is cryptographically signed at creation time. You can verify that an event has not been modified:
|
||||
When signing is available, Pulse signs audit events at creation time. Events
|
||||
captured without a signer remain unsigned. Use **Verify** in the signed-in
|
||||
Audit Log panel, or the authenticated read below, to check a stored signature:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
|
|
@ -132,7 +139,49 @@ Response:
|
|||
}
|
||||
```
|
||||
|
||||
If `verified` is `false`, the event data has been tampered with since it was recorded.
|
||||
`verified: true` means the signature matches the verifier's key and supported
|
||||
signature format. It does not prove that the history is complete or that no
|
||||
events were deleted. `verified: false` means the check failed, not that Pulse
|
||||
has established why it failed. Keep that result as evidence.
|
||||
|
||||
### Verification failures and safe recovery
|
||||
|
||||
| Result | What it establishes | Safe next step |
|
||||
| --- | --- | --- |
|
||||
| **Not checked** | No verification result has been obtained. | Use **Verify** for the relevant event. |
|
||||
| **Unsigned** | No signature was stored. | Check whether signing was available when the event was captured. Later enabling signing cannot authenticate it retroactively. |
|
||||
| **Failed** / `verified: false` | The stored event and signature do not verify with the current key and supported format. | Preserve the evidence; a different key, missing or damaged signature, unsupported format or modified signed fields can cause this. |
|
||||
| **Unavailable** / `available: false` | The current logger cannot offer verification. | Check the audit capability/runtime gate and bounded startup logs for persistent-store initialisation failures. |
|
||||
| **Error** / non-successful HTTP response | The verification request did not return a usable result. | Keep the HTTP status and redacted error; it is not proof that the event failed verification. |
|
||||
|
||||
Start with the event's time, the last known working version and whether the
|
||||
data mount, service account, runtime or backup changed. Inspect a bounded
|
||||
startup log excerpt locally; distinguish encryption/signing initialisation
|
||||
errors from an audit database/query error. Do not print the key files or post
|
||||
whole audit exports, service environments or backups. For an empty panel,
|
||||
clear filters and check the selected organisation before assuming events were
|
||||
lost. See [Getting Help](TROUBLESHOOTING.md#-getting-help) for safe evidence.
|
||||
|
||||
**Preserve before changing anything.** Keep a consistent private backup of the
|
||||
current data directory, including the audit store and its signing and
|
||||
encryption keys. Use your established snapshot/backup procedure for a running
|
||||
instance; copying a live `.db` file alone is not a consistent backup. For an
|
||||
offline filesystem backup, stop Pulse first and retain the complete store,
|
||||
including any SQLite sidecar files, with its original ownership and access.
|
||||
|
||||
Do not delete or regenerate a key, replace a live signing key with an older
|
||||
one, edit audit rows or re-sign old events to make verification pass. A newly
|
||||
generated signing key does not verify events signed with the previous key.
|
||||
The encrypted signing key also needs its matching `.encryption.key`; restoring
|
||||
only one member is not a recovery. Swapping keys can break verification of
|
||||
newer events while concealing the original problem.
|
||||
|
||||
If a matching historical backup exists, restore and compare it in an isolated
|
||||
test instance using the corresponding Pulse runtime/version, without
|
||||
replacing the live data or contacting monitored systems and notification
|
||||
destinations. Keep the current instance and both backups intact. If the
|
||||
original key is lost or an event was unsigned, do not claim that a restart,
|
||||
new key or successful check on a different event authenticated that history.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -160,16 +209,28 @@ See [Multi-Tenant Organizations](MULTI_TENANT.md) for details.
|
|||
| Export | License-gated (402) | Available |
|
||||
| `persistentLogging` API flag | `false` | `true` |
|
||||
|
||||
On all plans, audit events are written to the SQLite database. However, the query, verify, and export API endpoints require the `audit_logging` license feature and return `402 Payment Required` without it. The `persistentLogging` flag in API responses indicates whether the licensed query capabilities are available.
|
||||
Pulse attempts SQLite-backed capture on all plans; failure to initialise the
|
||||
store can leave console-only logging. The query, verify and export endpoints
|
||||
also require the `audit_logging` capability and a compatible runtime. A
|
||||
licence/runtime gate is not proof that events were never captured. For an
|
||||
authorised query, `persistentLogging` describes the active logger, not a count
|
||||
of historical events or proof that their signatures are valid.
|
||||
|
||||
---
|
||||
|
||||
## Storage
|
||||
|
||||
Audit events are stored in a SQLite database in the Pulse data directory:
|
||||
The default audit store lives in the Pulse data directory:
|
||||
- **Single-tenant:** `{data-dir}/audit/audit.db`
|
||||
- **Multi-tenant:** `{data-dir}/orgs/{org-id}/audit/audit.db`
|
||||
|
||||
The default encrypted signing key is `.audit-signing.key` **inside the audit
|
||||
store directory**, alongside `audit.db`, not at the data-directory root. Its
|
||||
matching `.encryption.key` is in the data directory (or the organisation's data
|
||||
directory for a non-default organisation). Keep both with the corresponding
|
||||
history. A runtime may supply a different audit directory or managed signing
|
||||
key; use that deployment's actual configuration rather than guessing a path.
|
||||
|
||||
Data directory locations:
|
||||
- systemd: `/etc/pulse/`
|
||||
- Docker/Kubernetes: `/data/`
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
| ------ | --------- | ---------------- |
|
||||
| `.env` | Authentication & Secrets | 🔒 **Critical** (Read-only by owner) |
|
||||
| `.encryption.key` | Encryption key for `.enc` files | 🔒 **Critical** |
|
||||
| `.audit-signing.key` | Audit log signing key (Pro/legacy Pro+/Cloud, encrypted) | 🔒 **Sensitive** |
|
||||
| `audit/.audit-signing.key` | Encrypted signing key for the default audit store; preserve with its history and matching encryption key | 🔒 **Sensitive** |
|
||||
| `system.json` | General Settings | 📝 Standard |
|
||||
| `nodes.enc` | Node Credentials | 🔒 **Encrypted** (AES-256-GCM) |
|
||||
| `alerts.json` | Alert Rules | 📝 Standard |
|
||||
|
|
@ -35,7 +35,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
| `sessions.json` | Persistent sessions (includes OIDC refresh tokens) | 🔒 **Sensitive** |
|
||||
| `update-history.jsonl` | Update history log (in-app updates) | 📝 Standard |
|
||||
| `metrics.db` | Persistent metrics history (SQLite) | 📝 Standard |
|
||||
| `audit.db` | Audit log database (Pro/legacy Pro+/Cloud, SQLite) | 🔒 **Sensitive** |
|
||||
| `audit/audit.db` | Default audit database; capture on all plans, licensed query/export | 🔒 **Sensitive** |
|
||||
| `baselines.json` | AI baseline data for anomaly detection | 📝 Standard |
|
||||
| `ai_correlations.json` | AI correlation analysis cache | 📝 Standard |
|
||||
| `ai_patterns.json` | AI pattern detection data | 📝 Standard |
|
||||
|
|
@ -45,7 +45,11 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
|
||||
Guest metadata entries are keyed by the canonical guest ID format `instance:node:vmid` (for example, `pve1:node1:100`). Legacy dash-separated keys are migrated automatically.
|
||||
|
||||
All files are located in `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by default.
|
||||
Paths are relative to `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by
|
||||
default. The audit database and default signing key are in the `audit/`
|
||||
subdirectory. Runtime-specific audit storage and non-default organisation
|
||||
paths can differ; see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage).
|
||||
Do not reset signing or encryption keys as a troubleshooting step.
|
||||
|
||||
Path overrides:
|
||||
- `PULSE_DATA_DIR` sets the base directory for `system.json`, encrypted files, and the bootstrap token.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Pulse uses a split config model:
|
|||
|
||||
- **Local auth and secrets**: `.env` (managed by Quick Security Setup or environment overrides, not shown in the UI)
|
||||
- **Encryption key**: `.encryption.key` (required to decrypt `.enc` files)
|
||||
- **Audit signing key**: `.audit-signing.key` (Pro/legacy Pro+/Cloud, encrypted)
|
||||
- **Audit signing key**: `audit/.audit-signing.key` (default store, encrypted; preserve with its matching encryption key and history)
|
||||
- **System settings**: `system.json` (editable in the UI unless locked by env)
|
||||
- **Nodes and credentials**: `nodes.enc` (encrypted)
|
||||
- **Notification config**: `email.enc`, `webhooks.enc`, `apprise.enc` (encrypted)
|
||||
|
|
@ -40,7 +40,7 @@ Pulse uses a split config model:
|
|||
- **AI pattern data**: `ai_patterns.json`
|
||||
- **AI remediation data**: `ai_remediations.json`
|
||||
- **AI incident tracking**: `ai_incidents.json`
|
||||
- **Audit log database**: `audit.db` (Pro/legacy Pro+/Cloud, SQLite)
|
||||
- **Audit log database**: `audit/audit.db` (default store; persistent capture on all plans, licensed query/export)
|
||||
- **Relay/Pro/legacy Pro+/Cloud license**: `license.enc` (encrypted)
|
||||
- **Host metadata**: `host_metadata.json`
|
||||
- **Docker metadata**: `docker_metadata.json`
|
||||
|
|
@ -64,6 +64,12 @@ Path mapping:
|
|||
- systemd/LXC: `/etc/pulse/*`
|
||||
- Docker/Helm: `/data/*`
|
||||
|
||||
The audit paths above are the default store, not a licence-dependent guarantee
|
||||
that storage initialised successfully. Runtime-specific storage can differ;
|
||||
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
|
||||
history or keys. Do not replace keys in a live instance to clear a verification
|
||||
failure.
|
||||
|
||||
Enterprise/internal multi-org layout:
|
||||
- Default org uses the root data dir for backward compatibility.
|
||||
- Non-default orgs use `/orgs/<org-id>/`.
|
||||
|
|
|
|||
|
|
@ -109,19 +109,40 @@ only the relevant port numbers and a redacted error if help is needed.
|
|||
- If another admin can log in, use `POST /api/security/reset-lockout` to clear the lockout for your username or IP.
|
||||
|
||||
#### Audit Log verification shows unsigned events
|
||||
- **Symptom**: Audit Log entries show “Unsigned” or verification fails in the UI.
|
||||
- **Root cause**: Audit signing is disabled (crypto manager unavailable), so events are stored without signatures.
|
||||
- **Fix**: Ensure `.encryption.key` is present and Pro/legacy Pro+/Cloud audit logging is enabled, then restart Pulse to regenerate `.audit-signing.key`. Newly created events will be signed; existing unsigned events remain unsigned.
|
||||
|
||||
**Unsigned** means no signature was stored for that event; it is not the same
|
||||
as **Failed** verification or a request **Error**. Signing can be unavailable
|
||||
when Pulse cannot initialise its encryption manager. Check a bounded startup
|
||||
log excerpt and the persistent data mount and access for the service account,
|
||||
without printing key contents. Do not delete or regenerate `.encryption.key`
|
||||
or an audit signing key to make the warning disappear. Restoring signing for
|
||||
new events cannot authenticate an old unsigned event.
|
||||
|
||||
See [Audit verification and safe recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery)
|
||||
for the different results and evidence to retain.
|
||||
|
||||
#### Audit Log is empty
|
||||
- **Symptom**: Audit Log shows zero events or "Console Logging Only."
|
||||
- **Root cause**: Community plan uses console logging only, or Pro/legacy Pro+/Cloud audit logging is not enabled.
|
||||
- **Fix**: Use Pro, legacy Pro+, or Cloud with audit logging enabled, then generate new audit events (logins, token creation, password changes).
|
||||
|
||||
Clear the event, user, date and success filters, and check the selected
|
||||
organisation first. A query error is not an empty history. **Pulse Pro runtime
|
||||
required** means an active licence is running on the public community runtime;
|
||||
follow the panel's **Download Pulse Pro** link rather than buying another
|
||||
licence or resetting storage. Without the audit capability, reads and exports
|
||||
are gated, but Pulse still attempts to capture events persistently on all
|
||||
plans. **Console Logging Only** can also reflect unavailable persistent
|
||||
storage: inspect the bounded startup logs for audit initialisation errors.
|
||||
Do not change passwords or create tokens merely to populate the panel.
|
||||
|
||||
#### Audit Log verification fails for older events
|
||||
- **Symptom**: Older events fail verification while newer events pass.
|
||||
- **Root cause**: The audit signing key changed (for example, `.audit-signing.key` was regenerated), so signatures no longer match.
|
||||
- **Fix**: Restore the previous `.audit-signing.key` from backup to verify older events. If rotated intentionally, expect older events to fail verification.
|
||||
|
||||
A failed signature check does not by itself prove tampering. An event signed
|
||||
with a different key can fail even when its contents are unchanged; missing
|
||||
signatures and an unsupported or damaged signature format also cannot verify.
|
||||
Keep the failure as evidence. Do not swap an old key into the live instance,
|
||||
edit audit rows or re-sign old events. Preserve the current data and keys
|
||||
privately before any recovery; compare a matching backup only in an isolated
|
||||
restore, not by overwriting today's history. Follow
|
||||
[safe audit recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery).
|
||||
|
||||
### Monitoring Data
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ Pulse's audit log records security-relevant events with tamper-evident signature
|
|||
|
||||
**Requires:** Pro, legacy Pro+, or Cloud license with the `audit_logging` capability to query, export, and verify events via the API. Events are recorded on all plans, but the API endpoints are license-gated.
|
||||
|
||||
An active licence alone does not enable the audit UI/API on the public
|
||||
community runtime. If the panel says **Pulse Pro runtime required**, follow
|
||||
**Download Pulse Pro** to the private runtime for your deployment. Do not buy
|
||||
a second licence or reset the data directory to clear this gate.
|
||||
|
||||
For plan details, see [PULSE_PRO.md](PULSE_PRO.md). For API endpoints, see [API Reference](API.md#-audit-log-pro).
|
||||
|
||||
---
|
||||
|
|
@ -116,7 +121,9 @@ sensitive data; keep it outside shared repositories and issue attachments.
|
|||
|
||||
## Tamper Detection
|
||||
|
||||
Every audit event is cryptographically signed at creation time. You can verify that an event has not been modified:
|
||||
When signing is available, Pulse signs audit events at creation time. Events
|
||||
captured without a signer remain unsigned. Use **Verify** in the signed-in
|
||||
Audit Log panel, or the authenticated read below, to check a stored signature:
|
||||
|
||||
```bash
|
||||
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
|
||||
|
|
@ -132,7 +139,49 @@ Response:
|
|||
}
|
||||
```
|
||||
|
||||
If `verified` is `false`, the event data has been tampered with since it was recorded.
|
||||
`verified: true` means the signature matches the verifier's key and supported
|
||||
signature format. It does not prove that the history is complete or that no
|
||||
events were deleted. `verified: false` means the check failed, not that Pulse
|
||||
has established why it failed. Keep that result as evidence.
|
||||
|
||||
### Verification failures and safe recovery
|
||||
|
||||
| Result | What it establishes | Safe next step |
|
||||
| --- | --- | --- |
|
||||
| **Not checked** | No verification result has been obtained. | Use **Verify** for the relevant event. |
|
||||
| **Unsigned** | No signature was stored. | Check whether signing was available when the event was captured. Later enabling signing cannot authenticate it retroactively. |
|
||||
| **Failed** / `verified: false` | The stored event and signature do not verify with the current key and supported format. | Preserve the evidence; a different key, missing or damaged signature, unsupported format or modified signed fields can cause this. |
|
||||
| **Unavailable** / `available: false` | The current logger cannot offer verification. | Check the audit capability/runtime gate and bounded startup logs for persistent-store initialisation failures. |
|
||||
| **Error** / non-successful HTTP response | The verification request did not return a usable result. | Keep the HTTP status and redacted error; it is not proof that the event failed verification. |
|
||||
|
||||
Start with the event's time, the last known working version and whether the
|
||||
data mount, service account, runtime or backup changed. Inspect a bounded
|
||||
startup log excerpt locally; distinguish encryption/signing initialisation
|
||||
errors from an audit database/query error. Do not print the key files or post
|
||||
whole audit exports, service environments or backups. For an empty panel,
|
||||
clear filters and check the selected organisation before assuming events were
|
||||
lost. See [Getting Help](TROUBLESHOOTING.md#-getting-help) for safe evidence.
|
||||
|
||||
**Preserve before changing anything.** Keep a consistent private backup of the
|
||||
current data directory, including the audit store and its signing and
|
||||
encryption keys. Use your established snapshot/backup procedure for a running
|
||||
instance; copying a live `.db` file alone is not a consistent backup. For an
|
||||
offline filesystem backup, stop Pulse first and retain the complete store,
|
||||
including any SQLite sidecar files, with its original ownership and access.
|
||||
|
||||
Do not delete or regenerate a key, replace a live signing key with an older
|
||||
one, edit audit rows or re-sign old events to make verification pass. A newly
|
||||
generated signing key does not verify events signed with the previous key.
|
||||
The encrypted signing key also needs its matching `.encryption.key`; restoring
|
||||
only one member is not a recovery. Swapping keys can break verification of
|
||||
newer events while concealing the original problem.
|
||||
|
||||
If a matching historical backup exists, restore and compare it in an isolated
|
||||
test instance using the corresponding Pulse runtime/version, without
|
||||
replacing the live data or contacting monitored systems and notification
|
||||
destinations. Keep the current instance and both backups intact. If the
|
||||
original key is lost or an event was unsigned, do not claim that a restart,
|
||||
new key or successful check on a different event authenticated that history.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -160,16 +209,28 @@ See [Multi-Tenant Organizations](MULTI_TENANT.md) for details.
|
|||
| Export | License-gated (402) | Available |
|
||||
| `persistentLogging` API flag | `false` | `true` |
|
||||
|
||||
On all plans, audit events are written to the SQLite database. However, the query, verify, and export API endpoints require the `audit_logging` license feature and return `402 Payment Required` without it. The `persistentLogging` flag in API responses indicates whether the licensed query capabilities are available.
|
||||
Pulse attempts SQLite-backed capture on all plans; failure to initialise the
|
||||
store can leave console-only logging. The query, verify and export endpoints
|
||||
also require the `audit_logging` capability and a compatible runtime. A
|
||||
licence/runtime gate is not proof that events were never captured. For an
|
||||
authorised query, `persistentLogging` describes the active logger, not a count
|
||||
of historical events or proof that their signatures are valid.
|
||||
|
||||
---
|
||||
|
||||
## Storage
|
||||
|
||||
Audit events are stored in a SQLite database in the Pulse data directory:
|
||||
The default audit store lives in the Pulse data directory:
|
||||
- **Single-tenant:** `{data-dir}/audit/audit.db`
|
||||
- **Multi-tenant:** `{data-dir}/orgs/{org-id}/audit/audit.db`
|
||||
|
||||
The default encrypted signing key is `.audit-signing.key` **inside the audit
|
||||
store directory**, alongside `audit.db`, not at the data-directory root. Its
|
||||
matching `.encryption.key` is in the data directory (or the organisation's data
|
||||
directory for a non-default organisation). Keep both with the corresponding
|
||||
history. A runtime may supply a different audit directory or managed signing
|
||||
key; use that deployment's actual configuration rather than guessing a path.
|
||||
|
||||
Data directory locations:
|
||||
- systemd: `/etc/pulse/`
|
||||
- Docker/Kubernetes: `/data/`
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
| ------ | --------- | ---------------- |
|
||||
| `.env` | Authentication & Secrets | 🔒 **Critical** (Read-only by owner) |
|
||||
| `.encryption.key` | Encryption key for `.enc` files | 🔒 **Critical** |
|
||||
| `.audit-signing.key` | Audit log signing key (Pro/legacy Pro+/Cloud, encrypted) | 🔒 **Sensitive** |
|
||||
| `audit/.audit-signing.key` | Encrypted signing key for the default audit store; preserve with its history and matching encryption key | 🔒 **Sensitive** |
|
||||
| `system.json` | General Settings | 📝 Standard |
|
||||
| `nodes.enc` | Node Credentials | 🔒 **Encrypted** (AES-256-GCM) |
|
||||
| `alerts.json` | Alert Rules | 📝 Standard |
|
||||
|
|
@ -35,7 +35,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
| `sessions.json` | Persistent sessions (includes OIDC refresh tokens) | 🔒 **Sensitive** |
|
||||
| `update-history.jsonl` | Update history log (in-app updates) | 📝 Standard |
|
||||
| `metrics.db` | Persistent metrics history (SQLite) | 📝 Standard |
|
||||
| `audit.db` | Audit log database (Pro/legacy Pro+/Cloud, SQLite) | 🔒 **Sensitive** |
|
||||
| `audit/audit.db` | Default audit database; capture on all plans, licensed query/export | 🔒 **Sensitive** |
|
||||
| `baselines.json` | AI baseline data for anomaly detection | 📝 Standard |
|
||||
| `ai_correlations.json` | AI correlation analysis cache | 📝 Standard |
|
||||
| `ai_patterns.json` | AI pattern detection data | 📝 Standard |
|
||||
|
|
@ -45,7 +45,11 @@ Pulse uses a split-configuration model to ensure security and flexibility.
|
|||
|
||||
Guest metadata entries are keyed by the canonical guest ID format `instance:node:vmid` (for example, `pve1:node1:100`). Legacy dash-separated keys are migrated automatically.
|
||||
|
||||
All files are located in `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by default.
|
||||
Paths are relative to `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by
|
||||
default. The audit database and default signing key are in the `audit/`
|
||||
subdirectory. Runtime-specific audit storage and non-default organisation
|
||||
paths can differ; see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage).
|
||||
Do not reset signing or encryption keys as a troubleshooting step.
|
||||
|
||||
Path overrides:
|
||||
- `PULSE_DATA_DIR` sets the base directory for `system.json`, encrypted files, and the bootstrap token.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Pulse uses a split config model:
|
|||
|
||||
- **Local auth and secrets**: `.env` (managed by Quick Security Setup or environment overrides, not shown in the UI)
|
||||
- **Encryption key**: `.encryption.key` (required to decrypt `.enc` files)
|
||||
- **Audit signing key**: `.audit-signing.key` (Pro/legacy Pro+/Cloud, encrypted)
|
||||
- **Audit signing key**: `audit/.audit-signing.key` (default store, encrypted; preserve with its matching encryption key and history)
|
||||
- **System settings**: `system.json` (editable in the UI unless locked by env)
|
||||
- **Nodes and credentials**: `nodes.enc` (encrypted)
|
||||
- **Notification config**: `email.enc`, `webhooks.enc`, `apprise.enc` (encrypted)
|
||||
|
|
@ -40,7 +40,7 @@ Pulse uses a split config model:
|
|||
- **AI pattern data**: `ai_patterns.json`
|
||||
- **AI remediation data**: `ai_remediations.json`
|
||||
- **AI incident tracking**: `ai_incidents.json`
|
||||
- **Audit log database**: `audit.db` (Pro/legacy Pro+/Cloud, SQLite)
|
||||
- **Audit log database**: `audit/audit.db` (default store; persistent capture on all plans, licensed query/export)
|
||||
- **Relay/Pro/legacy Pro+/Cloud license**: `license.enc` (encrypted)
|
||||
- **Host metadata**: `host_metadata.json`
|
||||
- **Docker metadata**: `docker_metadata.json`
|
||||
|
|
@ -64,6 +64,12 @@ Path mapping:
|
|||
- systemd/LXC: `/etc/pulse/*`
|
||||
- Docker/Helm: `/data/*`
|
||||
|
||||
The audit paths above are the default store, not a licence-dependent guarantee
|
||||
that storage initialised successfully. Runtime-specific storage can differ;
|
||||
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
|
||||
history or keys. Do not replace keys in a live instance to clear a verification
|
||||
failure.
|
||||
|
||||
Enterprise/internal multi-org layout:
|
||||
- Default org uses the root data dir for backward compatibility.
|
||||
- Non-default orgs use `/orgs/<org-id>/`.
|
||||
|
|
|
|||
|
|
@ -109,19 +109,40 @@ only the relevant port numbers and a redacted error if help is needed.
|
|||
- If another admin can log in, use `POST /api/security/reset-lockout` to clear the lockout for your username or IP.
|
||||
|
||||
#### Audit Log verification shows unsigned events
|
||||
- **Symptom**: Audit Log entries show “Unsigned” or verification fails in the UI.
|
||||
- **Root cause**: Audit signing is disabled (crypto manager unavailable), so events are stored without signatures.
|
||||
- **Fix**: Ensure `.encryption.key` is present and Pro/legacy Pro+/Cloud audit logging is enabled, then restart Pulse to regenerate `.audit-signing.key`. Newly created events will be signed; existing unsigned events remain unsigned.
|
||||
|
||||
**Unsigned** means no signature was stored for that event; it is not the same
|
||||
as **Failed** verification or a request **Error**. Signing can be unavailable
|
||||
when Pulse cannot initialise its encryption manager. Check a bounded startup
|
||||
log excerpt and the persistent data mount and access for the service account,
|
||||
without printing key contents. Do not delete or regenerate `.encryption.key`
|
||||
or an audit signing key to make the warning disappear. Restoring signing for
|
||||
new events cannot authenticate an old unsigned event.
|
||||
|
||||
See [Audit verification and safe recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery)
|
||||
for the different results and evidence to retain.
|
||||
|
||||
#### Audit Log is empty
|
||||
- **Symptom**: Audit Log shows zero events or "Console Logging Only."
|
||||
- **Root cause**: Community plan uses console logging only, or Pro/legacy Pro+/Cloud audit logging is not enabled.
|
||||
- **Fix**: Use Pro, legacy Pro+, or Cloud with audit logging enabled, then generate new audit events (logins, token creation, password changes).
|
||||
|
||||
Clear the event, user, date and success filters, and check the selected
|
||||
organisation first. A query error is not an empty history. **Pulse Pro runtime
|
||||
required** means an active licence is running on the public community runtime;
|
||||
follow the panel's **Download Pulse Pro** link rather than buying another
|
||||
licence or resetting storage. Without the audit capability, reads and exports
|
||||
are gated, but Pulse still attempts to capture events persistently on all
|
||||
plans. **Console Logging Only** can also reflect unavailable persistent
|
||||
storage: inspect the bounded startup logs for audit initialisation errors.
|
||||
Do not change passwords or create tokens merely to populate the panel.
|
||||
|
||||
#### Audit Log verification fails for older events
|
||||
- **Symptom**: Older events fail verification while newer events pass.
|
||||
- **Root cause**: The audit signing key changed (for example, `.audit-signing.key` was regenerated), so signatures no longer match.
|
||||
- **Fix**: Restore the previous `.audit-signing.key` from backup to verify older events. If rotated intentionally, expect older events to fail verification.
|
||||
|
||||
A failed signature check does not by itself prove tampering. An event signed
|
||||
with a different key can fail even when its contents are unchanged; missing
|
||||
signatures and an unsupported or damaged signature format also cannot verify.
|
||||
Keep the failure as evidence. Do not swap an old key into the live instance,
|
||||
edit audit rows or re-sign old events. Preserve the current data and keys
|
||||
privately before any recovery; compare a matching backup only in an isolated
|
||||
restore, not by overwriting today's history. Follow
|
||||
[safe audit recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery).
|
||||
|
||||
### Monitoring Data
|
||||
|
||||
|
|
|
|||
142
pkg/audit/signer_recovery_guidance_test.go
Normal file
142
pkg/audit/signer_recovery_guidance_test.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package audit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
|
||||
)
|
||||
|
||||
// These controls exercise the recovery guide's key/evidence distinctions with
|
||||
// the real signer and encryption manager. They use only synthetic, private
|
||||
// temporary directories: no SQLite store, installed instance or external call.
|
||||
func TestSignerRecoveryGuidance(t *testing.T) {
|
||||
dataDir := t.TempDir()
|
||||
writeEncryptionKey := func(dir string, value byte) *crypto.CryptoManager {
|
||||
t.Helper()
|
||||
key := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{value}, 32))
|
||||
if err := os.WriteFile(filepath.Join(dir, ".encryption.key"), []byte(key), 0o600); err != nil {
|
||||
t.Fatalf("write synthetic encryption key: %v", err)
|
||||
}
|
||||
manager, err := crypto.NewCryptoManagerAt(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("load synthetic encryption manager: %v", err)
|
||||
}
|
||||
return manager
|
||||
}
|
||||
manager := writeEncryptionKey(dataDir, 'A')
|
||||
auditDir := filepath.Join(dataDir, "audit")
|
||||
signer, err := audit.NewSigner(auditDir, manager)
|
||||
if err != nil {
|
||||
t.Fatalf("create original signer: %v", err)
|
||||
}
|
||||
event := audit.Event{
|
||||
ID: "synthetic-recovery-evidence",
|
||||
Timestamp: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC),
|
||||
EventType: "login",
|
||||
User: "synthetic-user",
|
||||
Path: "/api/login",
|
||||
Success: true,
|
||||
Details: "synthetic recovery control",
|
||||
}
|
||||
event.Signature = signer.Sign(event)
|
||||
if !signer.Verify(event) {
|
||||
t.Fatal("original event must verify before recovery comparisons")
|
||||
}
|
||||
storedKey, err := os.ReadFile(filepath.Join(auditDir, ".audit-signing.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("read synthetic encrypted key: %v", err)
|
||||
}
|
||||
|
||||
t.Run("reload preserves verification without regeneration", func(t *testing.T) {
|
||||
reloaded, err := audit.NewSigner(auditDir, manager)
|
||||
if err != nil || !reloaded.Verify(event) {
|
||||
t.Fatalf("reload failed to verify unchanged event: %v", err)
|
||||
}
|
||||
currentKey, err := os.ReadFile(filepath.Join(auditDir, ".audit-signing.key"))
|
||||
if err != nil || !bytes.Equal(storedKey, currentKey) {
|
||||
t.Fatal("reload must not replace the stored signing key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new signing key fails unchanged historical event", func(t *testing.T) {
|
||||
freshSigner, err := audit.NewSigner(filepath.Join(t.TempDir(), "audit"), manager)
|
||||
if err != nil {
|
||||
t.Fatalf("create unrelated signer: %v", err)
|
||||
}
|
||||
if freshSigner.Verify(event) {
|
||||
t.Fatal("a new key must not verify an unchanged old event")
|
||||
}
|
||||
if !signer.Verify(event) {
|
||||
t.Fatal("failed check with another key must not alter the original evidence")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("signing key alone cannot restore with different encryption key", func(t *testing.T) {
|
||||
restoreDir := t.TempDir()
|
||||
otherManager := writeEncryptionKey(restoreDir, 'B')
|
||||
restoreAuditDir := filepath.Join(restoreDir, "audit")
|
||||
if err := os.MkdirAll(restoreAuditDir, 0o700); err != nil {
|
||||
t.Fatalf("create isolated restore directory: %v", err)
|
||||
}
|
||||
keyPath := filepath.Join(restoreAuditDir, ".audit-signing.key")
|
||||
if err := os.WriteFile(keyPath, storedKey, 0o600); err != nil {
|
||||
t.Fatalf("copy synthetic encrypted key: %v", err)
|
||||
}
|
||||
if _, err := audit.NewSigner(restoreAuditDir, otherManager); err == nil {
|
||||
t.Fatal("wrong encryption key must not silently recover the signer")
|
||||
}
|
||||
after, err := os.ReadFile(keyPath)
|
||||
if err != nil || !bytes.Equal(after, storedKey) {
|
||||
t.Fatal("a failed restore must preserve the supplied encrypted key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("matching key pair verifies in isolated restore", func(t *testing.T) {
|
||||
restoreDir := t.TempDir()
|
||||
restoredManager := writeEncryptionKey(restoreDir, 'A')
|
||||
restoreAuditDir := filepath.Join(restoreDir, "audit")
|
||||
if err := os.MkdirAll(restoreAuditDir, 0o700); err != nil {
|
||||
t.Fatalf("create isolated restore directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(restoreAuditDir, ".audit-signing.key"), storedKey, 0o600); err != nil {
|
||||
t.Fatalf("copy synthetic encrypted key: %v", err)
|
||||
}
|
||||
restoredSigner, err := audit.NewSigner(restoreAuditDir, restoredManager)
|
||||
if err != nil || !restoredSigner.Verify(event) {
|
||||
t.Fatalf("matching isolated key pair failed to verify old event: %v", err)
|
||||
}
|
||||
if !signer.Verify(event) {
|
||||
t.Fatal("isolated restore must leave the original verifier usable")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unsigned is not retrospectively authenticated", func(t *testing.T) {
|
||||
unsigned := event
|
||||
unsigned.Signature = ""
|
||||
if signer.Verify(unsigned) {
|
||||
t.Fatal("working signer must not authenticate an unsigned old event")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("altered data and unsupported format remain failures", func(t *testing.T) {
|
||||
altered := event
|
||||
altered.Details = "altered synthetic evidence"
|
||||
if signer.Verify(altered) {
|
||||
t.Fatal("changed signed content must still fail verification")
|
||||
}
|
||||
unsupported := event
|
||||
unsupported.Signature = "v99:" + event.Signature
|
||||
if signer.Verify(unsupported) {
|
||||
t.Fatal("unsupported signature format must not be accepted")
|
||||
}
|
||||
if !signer.Verify(event) {
|
||||
t.Fatal("negative comparisons must preserve the original evidence")
|
||||
}
|
||||
})
|
||||
}
|
||||
86
scripts/tests/test_audit_recovery_docs.py
Normal file
86
scripts/tests/test_audit_recovery_docs.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Keep audit help from treating a failed check as a diagnosis or a reset task.
|
||||
|
||||
Signer/key behaviour is exercised separately by TestSignerRecoveryGuidance;
|
||||
these checks bind those distinctions to the actual canonical and shipped help.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
GUIDES = ("AUDIT_LOGGING", "TROUBLESHOOTING", "CONFIGURATION", "DEPLOYMENT_MODELS")
|
||||
|
||||
|
||||
def read_guide(name: str) -> str:
|
||||
return " ".join((ROOT / "docs" / f"{name}.md").read_text(encoding="utf-8").split())
|
||||
|
||||
|
||||
class AuditRecoveryDocsTest(unittest.TestCase):
|
||||
def test_shipped_guides_match_the_canonical_evidence(self):
|
||||
for name in GUIDES:
|
||||
with self.subTest(guide=name):
|
||||
self.assertEqual((ROOT / "docs" / f"{name}.md").read_bytes(),
|
||||
(ROOT / "frontend-modern/public/docs" / f"{name}.md").read_bytes())
|
||||
|
||||
def test_verification_results_are_not_conflated(self):
|
||||
guide = read_guide("AUDIT_LOGGING")
|
||||
for result in ("**Not checked**", "**Unsigned**", "**Failed**", "**Unavailable**", "**Error**"):
|
||||
with self.subTest(result=result):
|
||||
self.assertIn(result, guide)
|
||||
self.assertIn("not that Pulse has established why it failed", guide)
|
||||
self.assertIn("not proof that the event failed verification", guide)
|
||||
self.assertIn("does not prove that the history is complete", guide)
|
||||
self.assertNotIn("the event data has been tampered with since it was recorded", guide)
|
||||
|
||||
def test_recovery_preserves_history_and_both_keys(self):
|
||||
guide = read_guide("AUDIT_LOGGING")
|
||||
for boundary in ("consistent private backup", "copying a live `.db` file alone",
|
||||
"including any SQLite sidecar files", "matching `.encryption.key`",
|
||||
"isolated test instance", "without replacing the live data",
|
||||
"or contacting monitored systems and notification destinations",
|
||||
"Do not delete or regenerate a key", "edit audit rows or re-sign old events"):
|
||||
with self.subTest(boundary=boundary):
|
||||
self.assertIn(boundary, guide)
|
||||
self.assertIn("newly generated signing key does not verify events signed with the previous key", guide)
|
||||
self.assertIn("Later enabling signing cannot authenticate it retroactively", guide)
|
||||
|
||||
def test_troubleshooting_does_not_offer_a_live_key_swap(self):
|
||||
guide = read_guide("TROUBLESHOOTING")
|
||||
self.assertNotIn("restart Pulse to regenerate", guide)
|
||||
self.assertNotIn("Restore the previous `.audit-signing.key` from backup to verify", guide)
|
||||
self.assertIn("Do not swap an old key into the live instance", guide)
|
||||
self.assertIn("cannot authenticate an old unsigned event", guide)
|
||||
self.assertIn("AUDIT_LOGGING.md#verification-failures-and-safe-recovery", guide)
|
||||
|
||||
def test_empty_and_gated_are_not_missing_history(self):
|
||||
for name in ("TROUBLESHOOTING", "AUDIT_LOGGING"):
|
||||
guide = read_guide(name)
|
||||
with self.subTest(guide=name):
|
||||
self.assertIn("Pulse Pro runtime required", guide)
|
||||
self.assertIn("Download Pulse Pro", guide)
|
||||
self.assertIn("filters", guide)
|
||||
self.assertIn("organisation", guide)
|
||||
short = read_guide("TROUBLESHOOTING")
|
||||
self.assertIn("A query error is not an empty history", short)
|
||||
self.assertIn("Do not change passwords or create tokens merely to populate the panel", short)
|
||||
self.assertNotIn("Community plan uses console logging only", short)
|
||||
|
||||
def test_storage_paths_match_the_default_store_not_the_old_root_path(self):
|
||||
for name in ("CONFIGURATION", "DEPLOYMENT_MODELS"):
|
||||
guide = read_guide(name)
|
||||
with self.subTest(guide=name):
|
||||
self.assertIn("`audit/audit.db`", guide)
|
||||
self.assertIn("`audit/.audit-signing.key`", guide)
|
||||
self.assertIn("AUDIT_LOGGING.md#storage", guide)
|
||||
self.assertNotRegex(guide, r"(?:\| |\*\*: )`\.audit-signing\.key`")
|
||||
guide = read_guide("AUDIT_LOGGING")
|
||||
self.assertIn("**inside the audit store directory**", guide)
|
||||
self.assertIn("organisation's data directory", guide)
|
||||
self.assertIn("runtime may supply a different audit directory or managed signing key", guide)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue