Audit hash chain: signing/anchoring decision, enablement plan, chainer login, migration deploy gate (#41) #107
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Decisions + deploy gate for the audit hash chain (#41, off by default):
AuditLog:HashChain:Enabledon DEV/QA, and whether to chain the historical backlog (~100k rows/min). Run the SourceNode backfill first.20261109211317_AddAuditLogHashChainmust ship with the binaries, both centrals stopped (old binaries can't partition-purge once the columns exist; new binaries' per-channel purge fails without the migration). Pair with the other pending migrations (#102 item 1, #50 PrefixSearchIndexes).Owner decision 2026-11-09: item 1: no signing; periodically export the per-month chain head (hash + row count) off-box to a store/syslog the DB login can't write. Item 3: chainer and purge run as a separate maintenance login, not the writer. Items 2/4 (enablement, deploy gate) unchanged — they ride the coordinated migration window.
Item 4: another migration joins the coordinated central-pair window:
20261110032839_AddConfigGraphRowVersions(ScadaBridge#62,a4806576, scriptdocs/plans/sql/AddConfigGraphRowVersions.sql). Apply it afterAddAuditLogHashChain. It is purely additive: a non-concurrencyrowversioncolumn plus index on 18 configuration tables. SQL Server fills the existing rows as each column is added, a size-of-data operation per table on small tables. Old binaries ignore the column, so it adds no ordering constraint beyond going out in the same window.Items 1 and 3 implemented —
c4d7ec9b(main). Kept open for items 2 (enablement) and 4 (deploy gate).Item 1 — no signing; off-box head export (
AuditLog:HashChain:HeadExport, default off)IntervalMinutes(60), read every month's head in one snapshot under the chain lock SHARED — month,LastSeq,LastHash, purge anchor, live chained rows, tombstones — and append the heads that changed since that node's last export (all on the first tick;ExportUnchangedfor every tick). A failed write is logged and retried next tick.IAuditChainHeadSink):File— append-only JSONL,{Directory}/{yyyy}/audit-chain-heads-{yyyy-MM-dd}-{node}.jsonl, local or UNC, openedFileMode.Appendonly (grant the service account create + append data, nothing else);Syslog— RFC 5424 (<109>1 … chainhead - {json}, facility 13, severity Notice) over UDP or TCP (RFC 6587 octet counting).scadabridge audit verify-heads --path <file|dir>reads the records (file-sink lines or any syslog-collector log — JSON taken from first{to last}), collapses duplicates, sends the newVerifyAuditChainHeadscommand (OperationalAudit, unscoped). Per record:Matches/Extended/Purged(link removed by a partition switch → check the archive), or failuresMismatch/Regressed/MissingHead/MissingLink/CountMismatch→ exit 3.AuditChainHeadStoreTestsrewrites a chained row and re-links it, every later row and the head with the production hasher —verify-chainreports Intact,verify-headsreports Mismatch. Each other verdict has its own tampering case; sanctioned per-channel and partition purges stay non-failures. Mutation: removing the hash check or the count check fails the forgery / tamper tests. Sink tests use real UDP/TCP loopback listeners.Item 3 — maintenance login (
AuditLog:MaintenanceConnectionString,${secret:}-capable, unset = today's single login)AuditMaintenanceContext(keyedaudit-maintenancerepository/archive; the purge actor prefers them).AuditMaintenanceLoginTests(MSSQL) runs the hardened setup: writer login =db_datareader+db_datawriter+scadabridge_audit_writeringests and is refused the chain UPDATE and the per-channel DELETE; maintenance login =scadabridge_audit_chainer+scadabridge_audit_purgeronly chains, verifies, reads heads, purges per channel, enumerates partitions, archives and switches out. No new grants were needed. Mutation: putting the chain back on the runtime context fails withUPDATE permission was denied.audit backfill-source-node. Export failures are log-only so far (follow-up #115).Tests: AuditLog 462, ConfigurationDatabase 526, ManagementService 785, CLI 590, Host 671 — all green; build 0 warnings.
Item 4 — deploy gate: migrations added since this issue (2026-11-10). The coordinated central-pair window now carries, in order:
20261109203753_PrefixSearchIndexes(#50)20261109211317_AddAuditLogHashChain(#41)20261110032839_AddConfigGraphRowVersions(#62)20261110033732_FlipAuditLogClusteredKey(#111) — offline whole-table rebuild ofdbo.AuditLog(size the window from DEV/QA first; log ~2× table size). Scriptdocs/plans/sql/FlipAuditLogClusteredKey.sql, rollbackFlipAuditLogClusteredKey.rollback.sql. Gate: binaries from5e7d5389on purge either key shape; older binaries cannot partition-switch a flipped table (their staging table hard-codes the old key) — apply with both centrals stopped, together with the new binaries.Plus #102 item 1. #107 (
c4d7ec9b) and #109 add no migration: the head export, maintenance login and archive retention/cleanup are config only, default off (AuditLog:HashChain:HeadExport,AuditLog:MaintenanceConnectionString,AuditLog:Archive:RetentionDays/DeleteSupersededStaleArchives). Enabling the maintenance login needs the DBA steps in Component-AuditLog.md (chainer + purger roles on a second login, writer role on the runtime login).Deploy-gate list addition (item 4): migration
AuditMaintenanceGrants(533414a1, #113) grantsscadabridge_audit_purgerALTER ANY DATASPACE + UPDATE(SourceNode) on dbo.AuditLog — ships in the same coordinated central-pair window asAddAuditLogHashChain,AddConfigGraphRowVersions(#62) andFlipAuditLogClusteredKey(#111; requires new binaries, both centrals stopped).