# Translating Kafka ACLs (/connectors/kafka/how-to/acls)



KubeMQ does not keep a Kafka ACL store. Access for Kafka clients is written as KubeMQ authorization
rules — the same policy every KubeMQ connector uses — and this page is the translation table. The
Kafka ACL admin calls are answered honestly rather than faked: `kafka-acls --add` (`CreateAcls`) is
refused with `SECURITY_DISABLED` and a message naming the policy mechanism, and `DescribeAcls`
returns an empty list — so a script that provisions ACLs fails loudly instead of appearing to
succeed. Authorization is off by
default; it is enabled and supplied as described in
[Security → Authorization](/configure/reference/security#authorization).

## The rule shape [#the-rule-shape]

A policy is a JSON array of rules. For Kafka, every rule sets `EventsStore: true` — Kafka topics,
consumer groups and transactional ids all live there:

```json title="policy.json"
[
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "orders", "Read": false, "Write": true }
]
```

* **`ClientID`** is the authenticated principal: the SASL username, the OAUTHBEARER token's `sub`,
  or the client certificate's common name ([Authentication](/connectors/kafka/how-to/authentication)).
* **`Channel`** is the **bare** name of a topic, a consumer group, or a transactional id — there is
  no resource-type prefix, so a topic and a group with the same name share a rule.
* Both are **regular expressions matched against the whole value**: `orders` matches only
  `orders`; `orders-.*` matches every `orders-` topic; `.*` matches everything, including the `*`
  that stands for "the whole cluster" below.

With authorization on, a request with no authenticated principal is refused — there is no anonymous
access.

## Translation table [#translation-table]

| Kafka ACL                                          | KubeMQ rule                                                    |
| -------------------------------------------------- | -------------------------------------------------------------- |
| Topic `READ`                                       | `Read` on the topic                                            |
| Topic `WRITE`                                      | `Write` on the topic                                           |
| Topic `DESCRIBE`, `DESCRIBE_CONFIGS`               | `Read` on the topic                                            |
| Topic `CREATE`, `DELETE`, `ALTER`, `ALTER_CONFIGS` | `Write` on the topic                                           |
| Group `READ` (consume and commit offsets)          | `Read` &#x2A;*and `Write`** on the group — see below           |
| Group `DESCRIBE`                                   | `Read` on the group                                            |
| Group `DELETE`                                     | `Write` on the group                                           |
| TransactionalId `WRITE`                            | `Write` on the transactional id                                |
| Cluster `IDEMPOTENT_WRITE`                         | `Write` on channel `*` (a rule with `Channel: ".*"` covers it) |
| Cluster `DESCRIBE` (list groups, describe cluster) | `Read` on channel `*`                                          |

## Where it differs from Kafka [#where-it-differs-from-kafka]

Two operations need more than their Kafka equivalent:

* **Committing a consumer offset needs `Write` on the group.** Apache Kafka asks for Group `READ`.
  A consumer translated with only Read consumes but fails its first commit with
  `GROUP_AUTHORIZATION_FAILED`.
* **A transaction's offset commit needs `Write` on the group** (`AddOffsetsToTxn`,
  `TxnOffsetCommit`), where Kafka again asks for Read. Exactly-once pipelines — Kafka Streams with
  `exactly_once_v2`, consume-transform-produce — hit this first.

And one needs less spelling out: an **idempotent producer** — the default since Kafka 3.0 —
initialises with `Write` on the cluster channel `*`. Kafka 2.8+ accepts Topic Write in its place;
here, add the cluster rule.

## Worked example [#worked-example]

A Kafka ACL set for one service that consumes `orders`, writes `orders-enriched` transactionally,
and runs as `orders-service`:

```text title="Kafka"
User:orders-service  Topic:orders           READ
User:orders-service  Topic:orders-enriched  WRITE
User:orders-service  Group:enricher         READ
User:orders-service  TransactionalId:enricher-  WRITE   (prefixed)
```

```json title="KubeMQ policy"
[
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "orders",          "Read": true,  "Write": false },
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "orders-enriched", "Read": false, "Write": true },
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "enricher",        "Read": true,  "Write": true },
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "enricher-.*",     "Read": false, "Write": true },
  { "EventsStore": true, "ClientID": "orders-service", "Channel": "\\*",             "Read": false, "Write": true }
]
```

The group rule carries `Write` for the offset commits; the last rule is the idempotent-producer
cluster grant, written as `\\*` so it matches only the cluster channel rather than every name.

A denied request answers with the same codes Kafka uses: `TOPIC_AUTHORIZATION_FAILED`,
`GROUP_AUTHORIZATION_FAILED`, `TRANSACTIONAL_ID_AUTHORIZATION_FAILED` or
`CLUSTER_AUTHORIZATION_FAILED`. In a request that names several topics, only the denied ones fail.
