Query icon
OutputValues icon
Log icon
Return icon
Switch icon
Fail icon
SlackIncomingWebhook icon
If icon
Pause icon
Loop icon
Sleep icon
Schedule icon

Find and revoke over-broad Cassandra permission grants

Audit system_auth.role_permissions, skip superusers and an allowlist, then revoke over-broad grants behind an approval and re-read.

Categories
BusinessInfrastructure

Findings

A grant is over-broad when its scope is wider than any one service needs. This flow reads Cassandra's own grant table and applies three rules to every row:

resource in system_auth.role_permissions scope flagged
data every keyspace, present and future always
data/<keyspace> one whole keyspace only for a permission in flagged_keyspace_permissions
data/<keyspace>/<table> one table never
roles every role, so grants can be rewritten always
roles/<role> one role never

Two exclusions run before anything is reported. A superuser is skipped, because its rights come from the superuser bit rather than from a grant row, so revoking the row changes nothing it can do. A role and resource pair in grant_allowlist is skipped, because a deliberate wide grant must survive the guard. Both exclusions apply before the REVOKE list is built, so an excluded grant can never reach a statement.

Safety

Read only by default. auto_remediate is false and dry_run is true, so a first run prints the statement list and changes nothing. A REVOKE needs auto_remediate true, dry_run false and a person answering the Pause with approved true. An unanswered pause leaves approved null, which reads as not approved.

Cassandra accepts one permission per REVOKE, so the plan is one statement per role, resource and permission, run one at a time. On a keyspace-wide grant only the flagged permissions are revoked, so a reporting role keeps its SELECT. A role that lacks AUTHORIZE has its REVOKE refused by Cassandra while the task still reports green, which is why the re-read decides the outcome and fails the run.

How it works

  1. read_roles and read_grants (io.kestra.plugin.cassandra.standard.Query) read system_auth.roles and system_auth.role_permissions. Both are read only and both set allowFailure, so a cluster that cannot be reached is classified rather than crashing.
  2. audit_input puts the grant rows, the superuser bits, the allowlist and the flagged permission set into one JSON document. findings runs the scope rules over it and keeps the over-broad grants.
  3. classify turns the result into UNREACHABLE, CLEAN or OVER_BROAD. route (io.kestra.plugin.core.flow.Switch) branches on it.
    • CLEAN: clean logs the row count, so a quiet run still leaves a record.
    • UNREACHABLE: fail_unreachable fails the run, because a guard that cannot read the grant table is not a pass.
    • OVER_BROAD: alert posts the role, the scope and the permissions to Slack. plan builds one REVOKE per role, resource and permission plus the matching role|resource|permission keys. show_plan prints the statements.
  4. remediate_gate runs only when auto_remediate is true with dry_run false. hold (io.kestra.plugin.core.flow.Pause) waits for a person. remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs each REVOKE in turn.
  5. settle waits three seconds, reverify_read reads the grant table again, then reverify re-runs the same scope rules plus a check for the exact planned keys. verify_result fails the run through io.kestra.plugin.core.execution.Fail if any over-broad grant survived.
  6. The errors block posts to Slack when the guard itself fails, which is a different event from finding a wide grant.
  7. The every_six_hours Schedule trigger ships disabled: true and forces report only.

What you get

  • The real answer to who can read and write every keyspace on this cluster, read from the table Cassandra enforces rather than from the ticket where the grant was requested.
  • A named exclusion for superusers, so the report is about grants rather than about the superuser bit a separate control already covers.
  • An allowlist that a service owner can point at, so the guard can run unattended without threatening a grant that is meant to be wide.
  • One REVOKE per permission, run one at a time behind a human approval, with the grant table read back as proof and a failed run when the revoke did not land.
  • A Slack page naming the role, the scope and the permissions, plus a separate failure channel.

Who it's for

  • Platform and database teams who inherited a cluster and do not know what LIST ALL PERMISSIONS would say today.
  • Anyone who granted ALL PERMISSIONS ON ALL KEYSPACES during an incident and never took it back.
  • Compliance work that needs a dated record of effective Cassandra grants with least privilege evidence, not a screenshot of cqlsh.

Why orchestrate this with Kestra

LIST ALL PERMISSIONS in cqlsh answers this for one moment, for whoever happened to run it. It changes nothing. Kestra reads the whole grant table on a schedule, applies the scope rules with a superuser exclusion and an allowlist, posts the result to Slack, then applies the fix only behind two input gates plus a human approval, one statement at a time. The grant table is read back afterwards and the run fails when a REVOKE was refused, which matters because Cassandra refuses a REVOKE from a role without AUTHORIZE without raising an error the driver surfaces as a failure. A grant made next week is caught by the next scheduled run rather than by an audit.

Tested end to end

Run against Cassandra 5.0.9 with PasswordAuthenticator and CassandraAuthorizer enabled, with roles seeded through cqlsh and every result confirmed by LIST ALL PERMISSIONS OF <role> and a SELECT on system_auth.role_permissions rather than from the flow log.

  • Tidy cluster, only table-scoped and keyspace-read grants: 0 findings, CLEAN.
  • GRANT ALL PERMISSIONS ON ALL KEYSPACES TO w36_etl: the same inputs now report that grant, OVER_BROAD.
  • dry_run true: the statement list is printed and role_permissions is unchanged.
  • auto_remediate true with dry_run false and the pause approved: the statements run, the re-read reports 0 over-broad grants. The role's row is gone from the table.
  • Pause declined: the grant is still there afterwards.
  • Superuser holding the same wide grant: not reported, not revoked.
  • The same grant in grant_allowlist: not reported, not revoked.
  • Logging in as a role without AUTHORIZE: every REVOKE task reports green, the re-read still finds the grant. The run fails through fail_not_remediated with the Slack error block.

Prerequisites

  • Cassandra 4.x or 5.x with an authenticator and CassandraAuthorizer. With AllowAllAuthorizer the grant table stays empty and the guard has nothing to read.
  • A contact point reachable from the Kestra Worker on the native CQL port.

Secrets

Secret Used for
CASSANDRA_PASSWORD password for the role the flow logs in as
SLACK_WEBHOOK_URL Slack incoming webhook for the finding page and the failure page

Inputs

Input Type Default Purpose
cassandra_host STRING cassandra contact point hostname
cassandra_port INT 9042 native CQL port
cassandra_datacenter STRING datacenter1 local datacenter for the driver
cassandra_username STRING cassandra role the flow logs in as
cassandra_password STRING {{ secret('CASSANDRA_PASSWORD') }} password for that role
flagged_keyspace_permissions MULTISELECT AUTHORIZE, CREATE, ALTER, DROP, MODIFY permissions that make a keyspace-wide grant over-broad
grant_allowlist STRING empty role=resource pairs that must never be revoked
auto_remediate BOOL false allow REVOKE at all
dry_run BOOL true report only

Quick start

  1. Store CASSANDRA_PASSWORD and SLACK_WEBHOOK_URL as Kestra secrets.
  2. Run with the defaults. The role needs SELECT on system_auth, nothing more. Nothing is changed.
  3. Read show_plan in the log. It lists the exact REVOKE statements and the role each one hits.
  4. Put any deliberate wide grant into grant_allowlist as role=resource, using Cassandra's own resource names, then run again and confirm it is gone from the findings.
  5. To fix the rest, run with auto_remediate true and dry_run false using a role that holds AUTHORIZE, then approve the pause. Check remediated in the log and run LIST ALL PERMISSIONS OF <role> in cqlsh.
  6. Enable the Schedule trigger once a manual run looks right.

How to extend

  • Add functions and mbeans to the scope rules, with ALL FUNCTIONS and ALL MBEANS as the REVOKE scope, to cover UDF and JMX grants the same way.
  • Treat a keyspace-wide SELECT as over-broad on clusters holding personal data by adding SELECT to flagged_keyspace_permissions.
  • Flag a role whose grants arrive through member_of by walking role inheritance in system_auth.roles, so a wide grant on a parent role is attributed to its members.
  • Write each finding to a grant_audit table instead of Slack, then report on how long a wide grant stayed open.
  • Keep the allowlist in a Kestra KV pair instead of an input, so a service owner can change it without editing the flow.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.