Deny rules
Most of what the Gateway does is decide whether a result can come from cache. A deny rule does something different: it refuses the statement outright, at the Gateway, before your warehouse is ever contacted. Cache rules make queries cheaper. Deny rules make some queries not happen.
What a deny rule does
A rule with a deny action is a statement firewall. When an incoming
statement matches its conditions, the Gateway
answers the client with an error and stops. No cache lookup, no session, no
forward to the warehouse. The statement never runs.
Deny rules use the same condition vocabulary as cache rules — statement type, tables, schema, catalog, statement text, user, role, headers, bound parameters, client IP with CIDR matching, time of day and day of week. Anything you can write a cache rule against, you can write a deny rule against.
The client sees a normal SQL error
A blocked statement comes back in the client's own protocol, so it renders as a permissions error rather than a broken connection. This matters: a transport-level failure looks like an outage and generates a ticket, while a SQLSTATE the driver already understands looks like a policy decision and generates a conversation.
| Connection | What the client receives |
|---|---|
| Databricks (Thrift) | HTTP 200 carrying a Thrift error status — deliberately not a 403, which JDBC and ODBC would surface as a transport failure. |
| Databricks (JSON) | HTTP 200 with a failed statement state and a structured error. |
| Snowflake | The JSON error envelope Snowflake clients already parse. |
| PostgreSQL | A wire-protocol error frame at severity ERROR, not FATAL — the statement fails, the session survives. |
The defaults are chosen to be legible to any SQL client: SQLSTATE
42501 (insufficient_privilege) and the code
AIRBRX_STATEMENT_BLOCKED. You can override the SQLSTATE, the code,
and the message. The message is rendered verbatim by whatever tool the person is
sitting in front of, so it is capped in length and restricted to printable ASCII —
write it as something an analyst can act on, not an internal reference. "Exports
from customer_pii need an approved extract — ask #data-gov" beats "policy
violation".
Why deny rules are evaluated differently
Cache rules are priority-ordered and first-match-wins: the Gateway walks them in order, takes the first rule whose conditions match, and stops. That is correct for caching, where you want a specific rule to override a general one.
It would be dangerous for a block. If deny rules sat in the same ordered list, a cache rule with a higher-precedence priority could match first and the deny would never be reached — meaning a routine cache-tuning edit could silently disarm a security control. So the Gateway does not do that.
- Deny rules get their own pass, over every enabled rule. Not first-match-wins. Every deny rule is evaluated on every statement. Priority only decides which matched rule's message is returned to the client; every rule that matched is recorded.
-
The denial is attached last. After the cache decision, after
SQL hints. Nothing — including an
__AIRBRX_CACHE__hint in the statement text — can re-enable caching for a statement that is about to be refused. - It runs before invalidation. Invalidation markers are written as a side effect of evaluating a write. A denied statement never executes, so it must not evict cached results on behalf of a write that never happened.
Deny fails closed
The caching paths are deliberately forgiving: if rule evaluation fails, the Gateway degrades to passing the query through to the warehouse. A cache that stops working costs money; a cache that starts refusing traffic costs you your analytics.
Deny inverts that, because the failure modes are not symmetric. If the deny pass
itself throws, the statement is refused, with
AIRBRX_POLICY_EVALUATION_FAILED. The same applies to work that has to
happen before the deny pass can run: if the Gateway cannot parse a Postgres
statement and the tenant has deny rules configured, the statement is refused rather
than forwarded unexamined. A tenant with no deny rules keeps the forgiving
behavior; arming one is what switches the parse failure to fail-closed.
Bad deny rules are rejected at load, not discovered in traffic
A cache rule that degrades to "don't cache" costs money. A deny rule that degrades to "don't deny" disarms a control someone is relying on. So deny rules are validated strictly when the rule set loads, and a rule set containing an invalid one is refused rather than partially applied.
- An empty conditions object is rejected. A deny rule with no conditions would match every statement the tenant sends. That is never what anyone meant.
- Caching actions on a deny rule are rejected. A rule cannot both cache and deny, and cannot carry invalidation linkage. These are contradictions, not redundancies, so they fail rather than resolving to one interpretation.
- Malformed SQLSTATE, code, or severity values are rejected, as is a message that is too long or contains non-ASCII characters.
-
There is no shadow mode. An
auditOnlyflag was considered and cut. It is explicitly rejected rather than ignored — an author who writes it is expecting log-but-allow, and silently giving them enforced blocking instead is the worst possible outcome.
The last point is worth sitting with if you are used to policy engines that offer a dry-run. Airbrx does not have one for deny. To rehearse a rule, write it against a narrow condition set you control, or use the traffic page to confirm which statements would have matched before you widen it.
What gets recorded
A denial is emitted as a security event at the severity the rule's author chose, and rules set to critical route to alerting. Every deny rule that matched is named in that event, not just the one whose message the client saw.
One current limitation to know about: a denied statement does not yet produce a per-request traffic-log record, so blocked statements will not appear alongside hits and misses on the traffic page or in the analytics API. The security event is the record of a block today. If you need blocked statements in your own reporting, treat the security event stream as the source, not the traffic log.
What deny rules are not
Deny is a boundary at the Gateway, not a replacement for warehouse-side authorization. Anyone who can reach your warehouse directly — bypassing the gateway address — is unaffected by a deny rule. Row-level security, column masking, and grants are still the controls that hold when someone connects around you. Deny rules narrow what is possible through the path you have pointed your BI tools at; they do not narrow what is possible in your warehouse.
The rules engine currently has exactly two verbs: cache and deny. There is no routing action, no masking or redaction action, no rate-limit action, and no outbound-callout action. If you write an action key the Gateway does not implement, it is not silently ignored — it is logged as a critical event at rule load, so the gap surfaces immediately rather than being inferred from traffic weeks later.
Where to go next
-
Cache rule schema — the exact JSON
shape of the
denyaction. - Rules as the differentiator — how cache and invalidation rules pair up.
- Security posture — what Airbrx changes about your security model, and what it leaves alone.
Put a boundary in front of your warehouse
Deny rules are available on every plan. Create an account and arm your first one in the rules workshop.
Create an account