Fleet Table TRUNCATE Default-Privilege Automation: Flawless Row Policies, Emptiable Anyway

A policy that governs rows perfectly can still leave a door row-level security was never asked to guard
Row-level security is a powerful, precise tool for one specific job: deciding which ROWS a query is allowed to touch, for the commands it is configured to intercept. It says nothing, by design, about a database-wide default that hands out a broader set of table-level commands automatically the moment a table is created — and TRUNCATE is exactly that kind of command: a schema-level operation that empties a table in one step without evaluating a single row-level policy, because PostgreSQL never asks row-level security to weigh in on it in the first place. A team can review every policy on a table, find each one correct, and still miss that the table itself was never protected against a command row-level security was never in the loop for.
How the underlying problem shows up before you fix it
A database project's own default privilege setting grants a broad table-level command set — commonly including TRUNCATE — to public-facing roles automatically, on every table, the moment it is created, independent of any row-level security policy written for that table.
Row-level security policies are reviewed and found correct for the commands they actually intercept (typically SELECT, INSERT, UPDATE, DELETE) — and that correctness is mistaken for the table being fully protected, because TRUNCATE is easy to forget is a separate, schema-level privilege entirely.
The credential capable of triggering the gap is not a leaked secret — it is the public anonymous key, which is meant to be embedded in client-side code and is not a security boundary by itself.
The gap is invisible to a review of the row-level policies themselves, because there is nothing wrong with any individual policy — the exposure lives in a database-wide default that no single table's policy definition ever mentions.
The gap surfaces specifically after migrations are applied to a real database and the resulting privilege set is measured directly against that database — not from reading the migration SQL alone, since the SQL that creates a table and writes its policies never has to mention the default privilege that also applies to it.
How a same-day production check found what the SQL review could not
Centriu Guardian's fleet feature relies on 14 database tables — device records, alerts, incidents, enrollment codes, advocate relationships, warranty data, and more — each governed by row-level security policies scoping access to the organization a given authenticated user belongs to. Those policies were reviewed and found correct.
What the review did not surface, because nothing in the migration SQL for any individual table mentioned it, was the database project's own default privilege configuration: every newly created table in the public schema automatically receives a broad table-level privilege grant — commonly summarized as "all standard commands" — for both the anonymous and authenticated roles. That default includes TRUNCATE, and TRUNCATE is a schema-level operation in PostgreSQL, evaluated entirely outside of row-level security. A table can have policies restricting every SELECT, INSERT, UPDATE, and DELETE to exactly the right organization's own rows, and still be fully, instantly emptied by a single TRUNCATE statement that never triggers a single one of those policies.
The gap was found not by re-reading the SQL, but by applying the fleet migrations to a real database and measuring the resulting privilege set directly — the same discipline the source commit applied to a second, unrelated function-level gap (covered on a companion page). Confirmed: all 14 fleet tables carried this default privilege, meaning any caller holding only the anonymous key — public by definition, present in every browser bundle Guardian ships — could truncate any of them.
The fix revokes the full default privilege grant from both the anonymous and authenticated roles on every one of the 14 tables, then grants back, table by table, only the specific narrower command that table's own row-level policy is actually written to authorize — SELECT alone on some, SELECT plus UPDATE on others, and so on, matching each table's real access pattern rather than leaving a blanket grant in place. The anonymous role receives no direct table privilege on any of the 14 tables: Guardian's own device agent, which does use the anonymous key as its transport (it authenticates via a device token checked inside a function, not via a user session), talks to dedicated database functions for every write it needs to make — never to a table directly. Two of the fourteen tables, which have row-level security enabled but intentionally carry no policies at all, receive no grant whatsoever and are readable only through a dedicated function built for that specific, narrow purpose — a table with row-level security on and no policy, left with any standing grant, would have been an accidental wide-open door.
What is actually built today
All 14 Guardian fleet tables have their table-level privilege explicitly revoked from both the anonymous and authenticated database roles, replacing this project's automatic default privilege grant with a deliberately narrower one.
Each table's re-granted privilege matches exactly what that table's own row-level security policy is written to authorize — never a broader, catch-all grant left in place out of convenience.
The anonymous role holds no direct table-level privilege on any of the 14 fleet tables — every write the anonymous-key-authenticated device agent needs to make goes through a dedicated database function instead.
Two tables with row-level security enabled but zero policies defined — by design, meant to be read only through one specific function — carry no standing table grant at all, closing what would otherwise be an unintentional wide-open read (or truncate) path.
The fix required no schema change and no data migration — it is entirely a set of `revoke` and `grant` statements correcting which privileges apply, layered directly on top of the already-correct row-level policies.
A dedicated automated test reads the versioned migration SQL itself looking for the shape of this exact gap recurring — a table created without an accompanying privilege revoke — so a future table added to the fleet cannot silently reintroduce the same exposure.
One public key, every fleet table emptied (illustrative framing of the actual measured finding)
Before the fix, a caller equipped with nothing more than Centriu Guardian's public anonymous key — the same key present in the browser bundle of any page the agent or console ships — could issue a TRUNCATE against any of the 14 fleet tables and have it succeed instantly, without a single row-level policy ever being consulted, regardless of how correctly those policies scoped ordinary reads and writes. After the fix, the identical attempt is rejected outright: the anonymous role holds no privilege on the table at all, and the only path the device agent has for writing fleet data is through a dedicated function that checks a device-specific token internally before doing anything.
What changes operationally
Every one of Centriu Guardian's 14 fleet tables now carries table-level privilege that matches exactly what its own row-level security policy authorizes, with the anonymous role holding no standing privilege on any of them — closing a TRUNCATE path that existed entirely outside of, and invisible to, a correct row-level policy review.
When this is not the right fit
This automation governs the internal table-privilege configuration underneath Centriu Guardian's own fleet feature — it does not change how row-level security itself scopes reads and writes (that was already correct), does not add a customer-facing setting, and is specific to this project's own default-privilege configuration; a database configured with a narrower schema-level default from the start would not have needed this specific correction, though the underlying lesson about TRUNCATE bypassing row-level security applies broadly.
Trusting a schema-wide default vs. setting table privilege explicitly, table by table
Letting a database's default privilege setting apply automatically to every new table is convenient and, for many projects, never causes a problem — but the convenience trades away visibility: nothing in any individual table's own migration SQL discloses what the default actually grants, so a reviewer checking that table's policies has no local signal that a broader, schema-level command is also silently in play. Setting table privilege explicitly, matched to what each table's own policies are meant to authorize, costs a few extra lines of SQL per table and removes the gap entirely — the privilege a table has becomes something a reader of that table's own migration can see, not something they have to already know to go check elsewhere.
Related systems
Main system: Centriu Guardian.
What it does NOT do
- Does not change how Centriu Guardian's row-level security policies scope which rows a given user can read or write — those policies were already correct; this fix closes a separate, schema-level gap those policies were never positioned to catch.
- Does not add a new customer-facing access-control setting — the corrected privilege configuration was always the intended design for these internal fleet tables.
- Does not affect the device agent's own ability to write fleet data — it was already writing through dedicated functions, not direct table access, and continues to do so unchanged.
- Does not retroactively confirm whether any fleet table was actually truncated during the window this gap existed — a team with that concern would need its own database-level audit logging for the affected period, which is outside the scope of this fix.
- Does not overlap with the device-secret RPC execute-grant fix — that fix closes a different privilege gap, at the function level rather than the table level, from the same source commit.
Security and governance
Every one of Centriu Guardian's 14 fleet tables has table-level privilege explicitly revoked from the anonymous and authenticated roles and re-granted to match exactly what each table's own row-level security policy authorizes, with the anonymous role holding no standing table privilege at all. Any personal data referenced in fleet records remains subject to Brazil's LGPD (Law No. 13,709/2018). Full detail on access control lives at /governanca and /iso.
Pricing and contracting
Available by monthly subscription, with tiered plans. Values and terms come from the official pricing table at /precos (Centriu's central source — never restated here).
Frequently asked questions
Was this confirmed against a real database, or found by reading the migration SQL?
Confirmed directly against a real database: the fleet migrations were applied to production and the resulting table privileges were measured, which is how the gap was found — reading the migration SQL alone would not have surfaced it, since the default privilege that caused it is not written anywhere in that SQL.
Why did TRUNCATE bypass row-level security when every other command was correctly scoped?
TRUNCATE is a schema-level table operation in PostgreSQL, evaluated before and independent of row-level security policy checks — the database does not ask row-level security to weigh in on it at all, regardless of how correctly that table's policies scope other commands.
Could the anonymous key alone really trigger this?
Yes — the anonymous key is public by design (it ships inside client-side bundles) and is not meant to be a security boundary by itself; the gap here was that a database default handed a public-facing role a table-level privilege beyond what any row-level policy was designed to allow.
Did the device agent lose any functionality it needs?
No — the device agent was already authenticating writes through dedicated database functions using a device-specific token, not through direct table access, so removing the anonymous role's direct table privilege did not change how the agent operates.
Is this the same fix as the device-secret RPC page?
No — that page covers a function-level privilege gap (a stale nominal EXECUTE grant to the anonymous role) from the same source commit. This page covers a table-level privilege gap entirely — two different PostgreSQL privilege subsystems.
What does Centriu Guardian cost?
It is sold by subscription with a published starting price — exact current values are on the central pricing page.
See how Centriu Guardian scopes fleet table privilege
Reach our commercial team directly, or leave your details below — we'll follow up with guidance for your case.
