---
title: "Remove DENY ACLs"
description: "Remove DENY ACLs with delete-deny-acls, whose script re-checks effective permissions per ACL at run time. Wildcard DENYs stay."
url: https://monedula.dev/flock/docs/acl-rbac-converter/how-to/remove-deny-acls/
---

`delete-acls` never touches DENY ACLs. `delete-deny-acls` removes the ones you no longer need through a script that you run, which checks each DENY against MDS again at that moment and removes it only if that would not grant access.

## Before you start

- A plan made with `--allow-rejected`, which lists the DENY ACLs as rejected. See [Plan and review role bindings](/flock/docs/acl-rbac-converter/how-to/plan-and-review/).
- A `verify.json` for the current plan, with every result `EFFECTIVE_OK`, at its default place in the run directory, where the script reads it again. `delete-deny-acls` refuses while any result is missing or unknown.
- MDS credentials as a token file, or a user name and a password file. A token cached by `auth login` is not enough: the script may run hours later, and it has to authenticate without you.
- The bootstrap servers and a Kafka client properties file for a principal that may delete ACLs.
- A host with the run directory, `monedula-acl-rbac` and `kafka-acls` on the `PATH`, to run the script. If your Kafka tool has another name or location, such as `kafka-acls.sh`, set `MONEDULA_KAFKA_ACLS_BIN` to it.

## Steps

### 1. Check the DENY analysis

Read the `DENY analysis` section of `report.txt`. `plan` classifies every DENY ACL:

- `SAFE_TO_REMOVE`: no Allow ACL in the input grants the same principal the same operation on that resource.
- `WOULD_GRANT_ACCESS`: an Allow ACL does, so removing the DENY would open access.
- `UNKNOWN`: the analysis cannot tell, as for every wildcard principal.

Only `SAFE_TO_REMOVE` DENY ACLs can be removed by this tool.

### 2. Generate the script

```bash
monedula-acl-rbac delete-deny-acls \
  --plan runs/2026-05-21T10-00-00Z/plan.json \
  --verify runs/2026-05-21T10-00-00Z/verify.json \
  --mds-url https://mds.example.com \
  --mds-token-file ~/.confluent/token \
  --bootstrap-server kafka.example.com:9093 \
  --command-config admin.properties \
  --principal User:alice \
  --confirm \
  --i-understand-this-may-grant-access
```

`--principal`, or `--principal-file`, is required: there is no untargeted sweep. Both `--confirm` and `--i-understand-this-may-grant-access` are required too; `--i-understand-this-is-destructive` is not accepted on this command.

`delete-deny-acls` writes four files into the run directory:

- `delete-deny-acls.sh`: the deletion script.
- `deleted-deny-acls.json`: the DENY ACLs that the script may remove.
- `rollback-deny.sh`: a script that recreates them.
- `runtime.env`: the Kafka and MDS connection settings, the credential references and a per-run token, with mode 0600. Secrets are never written into the script or onto command lines.

### 3. Read the script

The script contains no bare `kafka-acls --remove` calls. It checks that `runtime.env` exists with mode 0600, loads it, and then runs one line per DENY:

```text
monedula-acl-rbac delete-deny-one --run-dir <absolute run directory> --acl-id <id>
```

`delete-deny-one` is an internal helper of this script, not a command you run yourself. For its ACL, it checks the plan and `verify.json` again, asks MDS whether the principal now holds any grant that overlaps the denied operation and resource, and removes the DENY only if none does. Because the check happens at run time, you can inspect, reorder or partly run the script by hand, even if access changed since you generated it.

### 4. Run the script

<Callout type="warning">
Removing a DENY can grant access to a principal that had none. Each removal is checked against MDS first, but keep `rollback-deny.sh` until you are sure.
</Callout>

```bash
bash runs/2026-05-21T10-00-00Z/delete-deny-acls.sh
```

Like the `delete-acls` script, it refuses to run, with exit code 5, when [`plan.json` changed or cannot be hashed](/flock/docs/acl-rbac-converter/concepts/safety-guarantees/#stale-plans-are-refused). Each DENY gets one line in `delete-deny.log`:

- `OK`: the live check found the DENY safe to remove, and it was removed.
- `SKIP` with `WOULD_GRANT_ACCESS`: something now grants the denied access, for example a binding added after you planned, so the DENY stays. There is no override.
- `SKIP` with `EFFECTIVE_UNKNOWN`: the live check failed, so the DENY stays.
- `FAIL`: the removal itself failed, and the script stops.

If MDS rejects the credentials, for example because the token expired, the script stops at that ACL instead of skipping the rest.

The script deletes `runtime.env` when it exits, so the per-run token does not outlive the run. To run it again, generate it again with `delete-deny-acls`.

## Wildcard DENYs stay

DENY ACLs for a wildcard principal, such as `User:*`, a bare `*` or any `<Type>:*`, are never removable by this tool. They cover principals the analysis cannot enumerate, so `plan` classifies them `UNKNOWN`, `delete-deny-acls` will not include them and there is no override. If you are certain that one must go, remove it by hand with `kafka-acls`.

## Check it worked

`delete-deny.log` has an `OK` line for each DENY that was removed and a `SKIP` line, with the reason, for each one that stayed. `kafka-acls --list` no longer shows the removed DENY ACLs. To restore them, run `rollback-deny.sh` against the same cluster.

## Related

- [Delete the converted ACLs](/flock/docs/acl-rbac-converter/how-to/delete-converted-acls/) handles the Allow ACLs.
- The [delete-deny-acls command reference](/flock/docs/acl-rbac-converter/reference/cli/delete-deny-acls/) lists every flag and exit code.