New to Kestra?
Use blueprints to kickstart your first workflows.
Diff live Kafka ACLs against a declared baseline, create the missing bindings with AclCreate behind a Pause, then re-list and fail if a grant is still absent.
Kafka authorization drifts in the direction nobody watches. A cleanup script written to remove one stale grant removes three. A cluster is rebuilt and the ACL restore stops half way. An audit prunes a wildcard ALLOW that a service was quietly relying on. Nothing breaks for the operator who ran the command. It breaks for a consumer group that starts logging TopicAuthorizationException at 03:00. The only record of what the principal used to hold is in somebody's shell history.
This blueprint makes the required grants a declared input and keeps the cluster matching it. AclList reads every live binding, one pass classifies each baseline entry. AclCreate adds back only what is missing, behind auto_remediate, dry_run and a Pause. A second AclList then re-reads the store, so a run only goes green when every baseline grant is really in force.
This blueprint was created by zkasuran.
| State | Meaning | Created by remediation |
|---|---|---|
present |
A live ALLOW binding covers this principal, resource and operation | No, nothing to do |
missing |
No live ALLOW covers it, no DENY stands in the way | Yes, one AclCreate |
blocked |
A live DENY covers it, so creating the ALLOW would not restore access | No, a human decides whether the DENY goes |
refused |
The entry itself is too broad (* literal, operation ALL, patternType MATCH) |
No, unless allow_broad_entries is on |
Coverage follows Kafka's own matching rules rather than string equality, so a baseline entry is present when a broader live grant already satisfies it: operation ALL, a PREFIXED pattern whose prefix the baseline name starts with, the * literal resource name or host *. That is what keeps the flow from piling a second narrow binding on top of a grant that already works.
AclCreate needs auto_remediate: true, dry_run: false and an approved Pause, in that order.AclCreate with permissionType: ALLOW. It never deletes a binding, never writes a DENY, never touches a principal that is not in the baseline.refused exists so a baseline cannot smuggle in the grants an ACL audit would take straight back out.Pause is there for blast radius. A missing grant is a reversible outage rather than a security hole, so the risk being gated is handing a principal access a human has not read, not losing data.list_acls (io.kestra.plugin.kafka.AclList, read-only) returns every binding on the cluster, ALLOW and DENY.plan normalizes the baseline input (patternType defaults to LITERAL, host to *) and classifies each entry as present, missing, blocked or refused in one jq pass.summary logs the four counts plus every missing grant in full.route (io.kestra.plugin.core.flow.If):in_force logs that the baseline is satisfied.alert posts the counts and the missing grants to Slack.restore_gate runs only when auto_remediate is true, dry_run is false and something is missing. Otherwise report_only logs what it would have created.approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved.apply_gate reads outputs.approve.onResume.approved ?? false, so an unanswered resume is not an approval. When it is false, declined logs that nothing was created.restore (io.kestra.plugin.core.flow.Loop at concurrencyLimit: 1) runs one io.kestra.plugin.kafka.AclCreate per missing binding.relist reads the ACL store again, verify re-runs the same classification against it. verify_gate fails the run through io.kestra.plugin.core.execution.Fail if any baseline grant is still not in force. On a clean result restored logs the new totals.errors posts to Slack on any failure, for example an unreachable broker or a principal that cannot alter ACLs.AclList and a failed run if a grant is still missing.AclList plus AclCreate with real gates around them.kafka-acls.sh --add restores a binding, but somebody has to notice it went. The baseline lives next to the flow as a typed input, so a change to it is a reviewed change. Kestra runs the diff on a schedule, keeps every run as evidence of what the cluster held that day, posts the drift to Slack, holds the write behind two inputs plus a human resume, then re-reads the cluster and marks the run red if the grants are still not there. The audit trail is the part a shell loop cannot give you.
On Kestra 2.0.5 OSS against Apache Kafka 3.9.1 in KRaft mode with StandardAuthorizer, with a local HTTP sink standing in for Slack. Every binding count below was read with the cluster's own kafka-acls.sh --list, before and after each run, not from the Kestra log. The seeded cluster holds the 6 baseline grants plus one unrelated User:metrics READ on heartbeat, which no run ever touched.
| Run | Cluster state and inputs | Result |
|---|---|---|
| 1 | baseline fully seeded, 7 bindings, defaults | 6 present, 0 missing. Nothing created, still 7 bindings |
| 2 | 3 bindings deleted (4 left), defaults | 3 missing found, Slack alert sent, nothing created, still 4 |
| 3 | same 4 bindings, auto_remediate: true, dry_run: true |
same 3 reported as creatable, still 4 bindings |
| 4 | same 4 bindings, auto_remediate: true, dry_run: false, resumed with approved: false |
declined logged, still 4 bindings |
| 5 | same 4 bindings, auto_remediate: true, dry_run: false, resumed with approved: true |
3 AclCreate calls, re-list clean, 7 bindings, the PREFIXED grant back as PREFIXED |
| 6 | 1 grant deleted plus a DENY seeded over a second one (6 bindings), approved | 1 missing plus 1 blocked. The missing one created (7 bindings), the run then failed on the re-verify naming the DENY |
| 7 | baseline edited to ask for * literal, operation ALL and patternType MATCH |
all 3 refused as too broad, 0 created |
| 8 | unreachable broker | list_acls failed, errors alert sent |
Runs 1 and 2 are the same flow with the same inputs against two cluster states, which is what shows the check can actually fail: 0 missing on the healthy cluster, 3 missing once the bindings were pulled out from under it.
DESCRIBE the cluster to list ACLs, plus ALTER on the cluster to create them. The read path needs only DESCRIBE.apache/kafka:3.9.1 with KAFKA_AUTHORIZER_CLASS_NAME=org.apache.kafka.metadata.authorizer.StandardAuthorizer plus KAFKA_SUPER_USERS=User:ANONYMOUS, then seed the baseline with kafka-acls.sh --add.SLACK_WEBHOOK_URL: used by alert and by the errors block. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_SLACK_WEBHOOK_URL.| Input | Default | Purpose |
|---|---|---|
bootstrap_servers |
localhost:9092 |
Brokers |
baseline |
6 example grants for 3 services | The ALLOW grants that must exist |
allow_broad_entries |
false |
Whether a wildcard or ALL baseline entry may be created |
auto_remediate |
false |
Allow AclCreate |
dry_run |
true |
Must be false for a create to run |
SLACK_WEBHOOK_URL secret.bootstrap_servers and replace baseline with the grants your services need. kafka-acls.sh --list on a healthy cluster is a good first draft.summary. Everything should be present.auto_remediate: true plus dry_run: false and approve the Pause.daily trigger, which always runs in safe mode.io.kestra.plugin.core.http.Request or a repository sync, so the contract has a review history.kv.Set of the last clean classification, then alert on a grant that goes missing twice in a week.blocked entry, since removing a DENY is a decision that needs an owner.