Detecting RBAC Drift Between Dev and Prod: A CI-Driven Matrix Diff System

You push to staging. It works. You deploy to prod. And suddenly, users can write to audit_logs. The problem? RBAC drift between environments — subtle, silent, and security-critical. This guide walks through detecting RBAC differences between dev and prod Hasura metadata by comparing generated Role × Field matrices. 1. Assumptions You export Hasura metadata from both environments: hasura metadata export --endpoint https://dev-api.example.com --output metadata-dev/ hasura metadata export --endpoint https://prod-api.example.com --output metadata-prod/ You have a matrix generator like rbac-matrix.js (see previous article) that produces: rbac-dev.csv rbac-prod.csv 2. Diff Goals We want to detect: ✅ Added/removed access ✅ Role gaining new permissions ✅ Field-level permission divergence ✅ Action type mismatches (e.g. SELECT allowed in dev, denied in prod) 3. Matrix Normalization Ensure both rbac-dev.csv and rbac-prod.csv are: Sorted by Role, Table, Field Have the same headers: Role,Table,Field,SELECT,INSERT,UPDATE,DELETE 4. Diff Script (TypeScript / Node.js) import fs from 'fs'; import { parse } from 'csv-parse/sync'; const dev = parse(fs.readFileSync('rbac-dev.csv'), { columns: true }); const prod = parse(fs.readFileSync('rbac-prod.csv'), { columns: true }); const key = (row: any) => `${row.Role}::${row.Table}::${row.Field}`; const mapDev = new Map(dev.map(row => [key(row), row])); const mapProd = new Map(prod.map(row => [key(row), row])); const allKeys = new Set([...mapDev.keys(), ...mapProd.keys()]); for (const k of allKeys) { const d = mapDev.get(k); const p = mapProd.get(k); if (!d) { console.log(`

Mar 30, 2025 - 14:11
 0
Detecting RBAC Drift Between Dev and Prod: A CI-Driven Matrix Diff System

You push to staging. It works.

You deploy to prod. And suddenly, users can write to audit_logs.

The problem?

RBAC drift between environments — subtle, silent, and security-critical.

This guide walks through detecting RBAC differences between dev and prod Hasura metadata by comparing generated Role × Field matrices.

1. Assumptions

  • You export Hasura metadata from both environments:
  hasura metadata export --endpoint https://dev-api.example.com --output metadata-dev/
  hasura metadata export --endpoint https://prod-api.example.com --output metadata-prod/
  • You have a matrix generator like rbac-matrix.js (see previous article) that produces:
    • rbac-dev.csv
    • rbac-prod.csv

2. Diff Goals

We want to detect:

✅ Added/removed access

✅ Role gaining new permissions

✅ Field-level permission divergence

✅ Action type mismatches (e.g. SELECT allowed in dev, denied in prod)

3. Matrix Normalization

Ensure both rbac-dev.csv and rbac-prod.csv are:

  • Sorted by Role, Table, Field
  • Have the same headers: Role,Table,Field,SELECT,INSERT,UPDATE,DELETE

4. Diff Script (TypeScript / Node.js)

import fs from 'fs';
import { parse } from 'csv-parse/sync';

const dev = parse(fs.readFileSync('rbac-dev.csv'), { columns: true });
const prod = parse(fs.readFileSync('rbac-prod.csv'), { columns: true });

const key = (row: any) => `${row.Role}::${row.Table}::${row.Field}`;

const mapDev = new Map(dev.map(row => [key(row), row]));
const mapProd = new Map(prod.map(row => [key(row), row]));

const allKeys = new Set([...mapDev.keys(), ...mapProd.keys()]);

for (const k of allKeys) {
  const d = mapDev.get(k);
  const p = mapProd.get(k);
  if (!d) {
    console.log(`