Most of what we write about here is "here's how something works." This one is "here's a gap we found in our own system and why it mattered" — because for a security product, that story is more useful to a prospective user than another feature list.
The two-layer design
XCloak's multi-tenancy model was designed with two independent layers of isolation:
- Application-level scoping — every repository query includes an explicit
WHERE tenant_id = $N, enforced in code, on every table that holds tenant data. - PostgreSQL row-level security (RLS) — database-level policies on the highest-value tables (agents, alerts, incidents, endpoint logs, Sigma rules, IOCs) that restrict which rows a query can see based on a session-level setting, independent of whether the application code remembered to filter correctly.
The idea behind having both: layer 1 is the thing that runs on every request and does the actual work, and layer 2 is the backstop that catches the case where layer 1 has a bug — a missed WHERE clause, a refactor that dropped a filter, a new query written without the pattern in mind. Defense in depth means the failure of one layer doesn't mean total failure.
What an internal review found
During a broader technical audit, we found that layer 2 wasn't actually load-bearing yet. The RLS policies existed in the database — migrated in, enabled, correctly defined — but the application wasn't consistently opening transactions in the way that makes those policies take effect (setting the tenant context via SET LOCAL on each request-scoped transaction). Layer 1 was doing all the real work; layer 2 was present but not wired into the request path that would have activated it.
To be clear about the actual risk here: this was not a tenant-isolation failure — application-level scoping was, and is, consistently applied and independently sufficient on its own. What was missing was the second, redundant layer that's supposed to be there in case the first one ever has a bug. The distinction matters, but it's not a reason to undersell the gap: "the backstop wasn't actually connected" is exactly the kind of finding that should get fixed before you tell people you have a backstop.
What changed
The fix is straightforward in concept even if it touches a lot of call sites: request-scoped transactions now set the tenant context explicitly, so the RLS policies that were already defined in the schema actually constrain what a query can return, not just what it's supposed to return by convention. This has been rolling out across the codebase in phases rather than as one big-bang change, specifically so each phase could be verified independently rather than trusting one large diff to get every call site right.
Why we're writing this instead of just quietly shipping the fix
Because "we found a gap in our own defense-in-depth and fixed it" is a more credible story than pretending the gap never existed — and because anyone doing real due diligence on a security product should be checking for exactly this kind of thing anyway. If we don't tell you where we found problems, the honest assumption should be that we didn't look hard enough, not that there weren't any.
If you're evaluating XCloak for a multi-tenant deployment, the current source is the place to verify where this stands, not this post — architecture writeups age faster than code does. The point of this post is the reasoning, not a permanent status report.