---
title: "How ACLs map to RBAC roles"
description: "Why Kafka ACLs and Confluent RBAC do not map one-to-one, how the default mapping rules work, and what ends up unmapped or rejected."
url: https://monedula.dev/flock/docs/acl-rbac-converter/concepts/how-acls-map-to-rbac/
---

A Confluent RBAC role binding grants a predefined role, a bundle of operations, so translating ACLs means choosing roles. monedula-acl-rbac chooses them with explicit rules and records every choice in the plan.

## What does not translate

Three properties of ACLs have no RBAC equivalent. A role always grants a set of operations, so an ACL for one operation needs a role that grants at least that operation, and possibly more. RBAC has no deny, so a DENY ACL cannot become a binding and its intent cannot be carried over. RBAC has no host-based scoping, so an ACL limited to one host could only be expressed by widening it to every host.

## From ACL rows to groups

The planner first groups the ACL rows that share everything except the operation: the same principal, host, resource type, resource name, pattern type and permission. A consumer's `Read` and `Describe` ACLs on one topic become one group with two operations. Each group remembers the IDs of its source ACLs, which is how every binding in the plan names the `source_acl_ids` it replaces.

Two expansions follow, both mirroring what the Kafka authorizer itself allows:

- An `All` ACL is expanded into the operations it implies for its resource type, for example `Read`, `Write`, `Create`, `Delete`, `Alter`, `Describe`, `DescribeConfigs` and `AlterConfigs` on a topic.
- On Allow groups, operations that imply others are expanded: `Read`, `Write`, `Delete` and `Alter` imply `Describe`, and `AlterConfigs` implies `DescribeConfigs`. A lone `Write` ACL therefore behaves like `Write` and `Describe`. Deny groups are not expanded, because denying `Write` does not deny `Describe`.

## Rules choose the roles

A rule says which role a group gets. Its `when` clause matches a group by operations, resource type and permission; its `then` clause names the role:

```yaml
rules:
  - when:
      operations: [Read, Describe]
      operations_mode: all   # default; alternatives: any
      resource_type: Topic
      permission_type: Allow
    then:
      role: DeveloperRead
      scope_template: kafka-cluster
```

With `operations_mode: all`, the default, the group must hold every listed operation; with `any`, one is enough. The rules are tried in order, and the planner selects them greedily: it takes the first rule that matches and covers at least one operation still uncovered, marks the operations that rule accounts for, its listed operations and the ones they imply, as covered, and continues while operations remain. Each distinct role it selects becomes one binding.

This is why a principal that both reads and writes a topic gets two bindings. No single Developer role grants both, so `DeveloperRead` covers `Read` and `Describe`, and `DeveloperWrite` then covers `Write`. RBAC bindings are additive, so the two together reproduce the original access. An `All` group stops after one rule: the broad `All` rules come first in the default list and cover every operation the group holds, so the group becomes a single `ResourceOwner` or `SystemAdmin` binding.

## The default rules

The tool ships these rules, in this order. `monedula-acl-rbac rules show` prints them as YAML.

| Source ACLs | Mode | Role |
| --- | --- | --- |
| `All` on `Cluster` | any | `SystemAdmin` |
| `All` on `Topic`, `Group` or `TransactionalId` | any | `ResourceOwner` |
| `Read` and `Describe` on `Topic` | all | `DeveloperRead` |
| `Write` and `Describe` on `Topic` | all | `DeveloperWrite` |
| `Read` and `Describe` on `Group` | all | `DeveloperRead` |
| `Write` and `Describe` on `TransactionalId` | all | `DeveloperWrite` |
| `DescribeConfigs` or `AlterConfigs` on `Topic` | any | `ResourceOwner` |
| `IdempotentWrite` on `Cluster` | any | `DeveloperWrite` |

Every default rule matches Allow ACLs only. Topic configuration access maps to `ResourceOwner` because RBAC cannot separate configuration rights. A superuser from CFK manifests arrives as `All` on the cluster and becomes `SystemAdmin`.

## Overriding the rules

A `rules.yaml` passed to `plan --rules` is merged into the defaults key by key. The key is the shape of the `when` clause: its operations, mode, resource type and permission. A rule with the same shape as a default replaces it in place, keeping its position. A rule with a new shape is added after the defaults, so it applies only where no default rule already covered the operations. `monedula-acl-rbac rules show > rules.default.yaml` gives you a starting point, and `init` writes a `rules.yaml` with a commented example that maps `All` on the cluster to `ClusterAdmin` instead of `SystemAdmin`.

Rules are checked when they are loaded: an unknown operation, resource type, mode or permission is an error, so a typo such as `read` for `Read` fails loudly instead of leaving ACLs unmapped.

## Scope is never widened

A binding keeps the resource pattern of its source ACLs exactly. A `PREFIXED` ACL on `Topic:foo` produces a binding on the prefix `foo`, never a cluster-wide grant, and a `LITERAL` ACL a literal binding. The cluster scope comes from `scopes.yaml` and the resource type: bindings on Kafka resources are scoped to `kafka_cluster`, and bindings on Schema Registry subjects to `schema_registry_cluster`.

## Principals

A principal passes through unchanged unless `principals.yaml` maps it, for example to the name MDS knows for a TLS distinguished name or an LDAP group. [Config files](/flock/docs/acl-rbac-converter/reference/config-files/) describes the mapping, the `fail` fallback and the `MTLS_DN_PASS_THROUGH` warning.

## Where every ACL ends up

Every input ACL lands in exactly one list of the plan, so none is silently dropped:

- **`bindings`**: covered by a rule. The action is `CREATE`, or `SKIP_EXISTS` when an existing binding found during extraction, for example in CFK manifests, already grants it.
- **`unmapped`**: not convertible as it stands, with a reason. `NO_RULE_MATCH` means no rule covers the group's operations, as for a lone `Create` on the cluster. `HOST_RESTRICTED` means the ACL is limited to a host. `CONFLICTING_EXISTING_BINDING` means an existing binding gives the principal the same role on other resources.
- **`rejected`**: never converted. DENY ACLs are rejected with `DENY_PERMISSION`.

When the selected roles still leave an operation ungranted, for example a `Delete` that no rule covers next to `Read` and `Describe`, the plan records a `PARTIAL_RULE_COVERAGE` warning: that grant is not converted, and the warning says so. `plan` exits 3 while anything is unmapped or rejected; [Plan and review role bindings](/flock/docs/acl-rbac-converter/how-to/plan-and-review/) shows how to resolve or accept it.

For each DENY, the plan also records a DENY analysis, `SAFE_TO_REMOVE`, `WOULD_GRANT_ACCESS` or `UNKNOWN`, which decides what [Remove DENY ACLs](/flock/docs/acl-rbac-converter/how-to/remove-deny-acls/) may remove later.

## Related

- The [rules show command reference](/flock/docs/acl-rbac-converter/reference/cli/rules-show/) and the [plan.v1.json format reference](/flock/docs/acl-rbac-converter/reference/formats/plan-v1/).
- Our article [From Kafka ACLs to Confluent RBAC](/blog/kafka-acls-confluent-rbac-migration-monedula-acl-rbac-converter/) explains why we built the tool around a reviewable workflow.