docs(audit-log): document append-only enforcement honestly (CI guard default; DB roles are optional DBA hardening; purger now has CREATE TABLE + scoped DELETE)
This commit is contained in:
@@ -441,13 +441,33 @@ MS SQL for direct-write events). Unredacted secrets never persist.
|
||||
|
||||
## Security & Tamper-Evidence
|
||||
|
||||
- **Append-only enforcement.** The application accesses `AuditLog` via a
|
||||
dedicated DB role `scadabridge_audit_writer` granted `INSERT` + `SELECT` only —
|
||||
no `UPDATE`, no `DELETE`. Purge runs under a separate role
|
||||
`scadabridge_audit_purger` whose permissions are limited to the partition-switch
|
||||
operation; row-level `DELETE` is not granted even to purge.
|
||||
- **CI grep guard.** The build greps the data layer for any
|
||||
`UPDATE … AuditLog` or `DELETE … AuditLog` text and fails on a hit.
|
||||
- **Append-only enforcement — what actually holds in the default deployment.**
|
||||
ScadaBridge runs with **one** connection principal for both the writer and the
|
||||
maintenance/purge paths (the runtime does not open a second, lower-privilege
|
||||
connection for purge). So in the default deployment the append-only invariant is
|
||||
enforced by two application-level controls, not by database permissions:
|
||||
1. **CI grep guard** — `AuditLogAppendOnlyGuardTests` scans the ConfigurationDatabase
|
||||
source for any `UPDATE`/`DELETE` DML targeting `dbo.AuditLog` and fails the build
|
||||
on a hit. Exactly two maintenance-path mutations are allow-listed by an explicit
|
||||
`// AUDIT-PURGE-ALLOWED` marker (the per-channel retention `DELETE TOP` and the
|
||||
one-time `SourceNode` sentinel backfill `UPDATE`); every other UPDATE/DELETE trips
|
||||
the guard.
|
||||
2. **Code review** — the marker is deliberately specific so an unrelated mutation
|
||||
cannot inherit the exemption without a reviewer noticing.
|
||||
- **Optional DBA hardening (two DB roles).** For a deployment that wants
|
||||
database-level enforcement on top of the application controls, migrations provision
|
||||
two roles: `scadabridge_audit_writer` (`INSERT` + `SELECT` only; `DENY UPDATE` +
|
||||
`DENY DELETE` so a later `db_datawriter` membership cannot silently re-enable
|
||||
mutation) and `scadabridge_audit_purger` (the maintenance principal). To use them,
|
||||
a DBA provisions **two logins** — the runtime connection mapped to the writer role,
|
||||
and a separate maintenance job/connection mapped to the purger role. The purger role
|
||||
now carries the permissions its purge path genuinely needs: `SELECT` + `ALTER ON
|
||||
SCHEMA::dbo` **plus** `CREATE TABLE` (the switch-out dance CREATEs a staging table)
|
||||
and a scoped `DELETE ON dbo.AuditLog` (the per-channel retention override is a bounded
|
||||
row DELETE) — granted by the `FixAuditPurgerRoleGrants` migration (arch-review 04, S3).
|
||||
Without those two grants the purger role could not actually execute the switch-out or
|
||||
the per-channel purge; the earlier claim that "row-level `DELETE` is not granted even
|
||||
to purge" was stale once `PerChannelRetentionDays` shipped.
|
||||
- **Authorization.** Reading the Audit Log requires the existing **Audit** role
|
||||
extended with a new **OperationalAudit** permission. Per-site row scoping
|
||||
reuses the existing site-permission model; bulk export requires an additional
|
||||
|
||||
Reference in New Issue
Block a user