v1.6.1
IntentGate Gateway v1.6.1
A patch release fixing three bugs in v1.6.0 that affect deployments using the Postgres backends for audit, approvals, or policy stores. Customers using only the in-memory backends are unaffected, but we recommend upgrading anyway since most production deployments switch to Postgres for replica-safety and persistence.
docker pull ghcr.io/netgnarus/intentgate-gateway:1.6.1The companion intentgate-helm chart will be bumped in a follow-up release.
Fixes
Fresh-install migration failure on policy_active
INTENTGATE_POLICY_STORE=postgres against a brand-new database failed migration with:
policystore: migrate: ERROR: there is no unique or exclusion constraint
matching the ON CONFLICT specification (SQLSTATE 42P10)
The v1.5 CREATE TABLE IF NOT EXISTS policy_active block didn't declare a primary key — the schema relied on an ALTER TABLE migration step that only fires when upgrading FROM v1.4 (which had the PK on id). Fresh installs hit neither code path and the bottom-of-file seed INSERT ... ON CONFLICT (tenant) DO NOTHING had nothing to conflict against.
Fix: PRIMARY KEY (tenant) added to the CREATE TABLE. The 1.4→1.5 migration block is preserved for in-place upgrades.
Who is affected: any deployment standing up INTENTGATE_POLICY_STORE=postgres for the first time against an empty database. Existing v1.4 → v1.5 → v1.6 upgrades aren't affected because the migration block already moved their PK to (tenant) correctly.
Audit chain verify always failed on the first row
/v1/admin/audit/verify always returned ok=false with "hash mismatch (row body tampered)" on row 1, even on a chain that was provably untampered.
The gateway hashes the canonical event including its RFC3339Nano timestamp (9 digits of fractional seconds). Postgres TIMESTAMPTZ stores at microsecond precision (6 digits). When VerifyChain reads the row back, the round-tripped time.Time is missing the last 3 digits, the recomputed hash covers a different string than what was stored, and every chain reported a tampered first row.
Fix: Truncate(time.Microsecond) applied to the timestamp before computing the canonical hash at insert time, so the persisted string matches what verify will see after the database round-trip.
Who is affected: any deployment with INTENTGATE_AUDIT_PERSIST=true. The verify endpoint surfaced this on every chain. Audit data integrity itself was unaffected — the chain hashes were internally consistent; verify just couldn't reconstruct them from the database.
Escalate events broke the audit chain
After the first fix above, chains verified clean until the first escalate event — at which point verify reported a hash mismatch on the escalate row.
The canonical hash includes pending_id, decided_by, and requires_step_up (the approval-flow fields populated by runApprovalFlow), but the audit_events schema had no columns for them. The insert path hashed them in, the database silently discarded them, and verify's reconstruction couldn't recover them. The same class of bug also affected elevation_id — the column existed but VerifyChain's SELECT was missing it, so any admin event under JIT elevation would have broken the chain too (latent in v1.6.0 because the e2e tests didn't exercise an elevated escalate).
Fix: added pending_id, decided_by, requires_step_up columns to audit_events with idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS migrations. Insert writes them; VerifyChain's SELECT + Scan reconstruct them along with the previously-missed elevation_id so the canonical hash is reproducible.
Who is affected: any deployment using INTENTGATE_APPROVALS_BACKEND=postgres (escalate path) or JIT admin elevation. The chain failed deterministically on the first such event.
Upgrade
In-place upgrade is safe. The migration block re-applies idempotently — IF NOT EXISTS on every CREATE TABLE and ALTER TABLE — so restarting against an existing database adds the new columns without data loss.
Existing audit chains written by v1.6.0 are unverifiable by design (the canonical-form mismatch means stored hashes can't be reproduced). v1.6.1 doesn't attempt to repair them; new events written after the upgrade form a new chain that verifies clean. SOC analysts wanting a tamper-evident audit trail should start their evidence window after the v1.6.1 upgrade. For deployments where this matters, optionally re-issue any compliance evidence packs that referenced v1.6.0 chain verification.
What we learned
All three bugs are in code paths that the unit test suite doesn't exercise against a real Postgres — the chain tests run against the in-memory store, and the policy store's existing Postgres test reused a long-lived database whose schema had already been applied at an earlier version. v1.6.1 adds internal/auditstore/postgres_test.go with four regression tests gated on INTENTGATE_TEST_POSTGRES_URL, each dropping the chain tables first so the migration runs against a truly empty database. These would have caught every one of the three bugs.
The deeper takeaway is that the existing pre-tag verify scripts targeted memory backends. The next CI iteration will run the full integration suite against a Postgres service container so this class of regression can't ship again.
Contact
j.cordoba@netgnarus.com for direct questions, security@netgnarus.com for disclosures.