# Users and permissions (/connectors/rabbitmq/how-to/users-and-permissions)



A migrating RabbitMQ deployment usually has its own users, each with a per-vhost
configure/write/read permission triple and a set of tags. The KubeMQ RabbitMQ connector can be
given the same thing through &#x2A;*`Connectors.Amqp.Credentials`** — a connector-local user store
consulted by SASL `PLAIN`/`AMQPLAIN` logins, by certificate logins over `EXTERNAL`, and by the
[management HTTP API](/connectors/rabbitmq/reference/management-api). This guide covers how to
configure it and what changes the moment you do.

<Callout type="warn">
  **Configuring users changes the authorization subject of every AMQP connection.** With no users
  configured, an authenticated client's subject is its platform token's client id. With at least
  one user configured, every `PLAIN`/`AMQPLAIN` connection's subject becomes its **encoded**
  username under the `amqp-_user-` prefix instead — and every existing authorization policy
  written against the token's client id stops matching, silently. Update the policy file in the
  same change. See [Which subject a policy must name](#which-subject-a-policy-must-name).
</Callout>

## The credential store [#the-credential-store]

`Connectors.Amqp.Credentials` is a list of entries, each with a `Username`, a `Password`, an
optional `Permissions` list and an optional `Tags` list. It is **file or structured
configuration only — there is no environment variable**, matching the Kafka and AWS connectors'
credential lists.

```toml title="config.toml"
[[Connectors.Amqp.Credentials]]
  Username = "orders_producer"
  Password = "s3cret"
  Tags = ["management"]
  [[Connectors.Amqp.Credentials.Permissions]]
    Vhost = "/"
    Configure = "^$"
    Write = "^orders\\.?"
    Read = "^$"

[[Connectors.Amqp.Credentials]]
  Username = "admin"
  Password = "adminpass"
  Tags = ["administrator"]
  [[Connectors.Amqp.Credentials.Permissions]]
    Vhost = ""        # wildcard: every vhost (this connector's own extension)
    Configure = ".*"
    Write = ".*"
    Read = ".*"
```

What the store changes:

* **Not configured (the default): nothing changes.** The SASL password is a KubeMQ platform token
  when authentication is on, and the username is recorded but never checked.
* **Configured: both the username and the password are checked** against this store, over
  `PLAIN` or `AMQPLAIN`, **whatever `Authentication.Enable` is set to** — the store is consulted
  before the platform flag is looked at, and the platform token is not consulted at all for a
  connection authenticating this way.
* **Certificate logins consult it too.** With a store configured, a certificate over `EXTERNAL`
  whose derived identity names no configured user is refused, so turning on certificate login
  does not create a second population of principals the store does not know about.

**Credential checks are constant-time, and the three ways to fail are indistinguishable on the
wire.** A wrong password, an unknown username, and no matching entry all close the connection
with the identical `connection.close(403)` / `ACCESS_REFUSED - authentication failed`, matching
RabbitMQ 4.3.4. The distinguishing reason goes to the audit log only.

**Validation** (at boot, and on every settings save even when the connector is off): every
`Username` and `Password` must be non-empty, usernames must not repeat, and no username may end
in the reserved `kubemq-dashboard` identity. Every non-empty permission pattern must compile as
an RE2 regular expression, no two entries for one user may name the same vhost, and no tag may
be an empty string.

## Permissions [#permissions]

A RabbitMQ user carries `rabbitmqctl set_permissions -p <vhost> <user> <conf> <write> <read>` —
a configure, a write and a read regular expression, per vhost. A credential can carry the same
triple as `Permissions`, a list of `{Vhost, Configure, Write, Read}` entries, enforced by the
connector **before** platform authorization runs.

A `set_permissions` invocation maps directly onto one entry:

```bash
# RabbitMQ
rabbitmqctl set_permissions -p / orders_producer "^$" "^orders\.?" "^$"
```

```json title="the same entry in Connectors.Amqp.Credentials"
{
  "Username": "orders_producer",
  "Password": "...",
  "Permissions": [
    { "Vhost": "/", "Configure": "^$", "Write": "^orders\\.?", "Read": "^$" }
  ]
}
```

A definitions export's `permissions` array maps the same way, field for field — each entry's
`vhost`, `configure`, `write` and `read` become one `Permissions` entry.

### What each verb governs [#what-each-verb-governs]

The operation → verb table, in check order. A passive declare needs **any one** of the three
verbs, and its denial always names `configure` — RabbitMQ's own reply.

| Operation                                                   | Check(s), in order                                                                         |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `queue.declare`, active                                     | `configure` on the queue                                                                   |
| `queue.declare`, passive                                    | any one of `configure`/`write`/`read` on the queue                                         |
| `queue.declare` with `x-dead-letter-exchange` (new queue)   | `configure` on the queue; then `read` on the queue and `write` on the dead-letter exchange |
| `queue.bind` / `queue.unbind`                               | `write` on the queue, then `read` on the exchange                                          |
| `queue.purge`                                               | `read` on the queue                                                                        |
| `queue.delete`                                              | `configure` on the queue                                                                   |
| `exchange.declare`, active                                  | `configure` on the exchange                                                                |
| `exchange.declare`, passive                                 | any one of `configure`/`write`/`read` on the exchange                                      |
| `exchange.declare` with `alternate-exchange` (new exchange) | `configure` on the exchange; then `read` on the exchange and `write` on the alternate      |
| `exchange.bind` / `exchange.unbind`                         | `write` on the destination, `read` on the source                                           |
| `exchange.delete`                                           | `configure` on the exchange                                                                |
| `basic.publish`                                             | `write` on the exchange (the default exchange `""` is reported as `amq.default`)           |
| `basic.get` / `basic.consume`                               | `read` on the queue                                                                        |
| `basic.consume` on `amq.rabbitmq.reply-to`                  | **no permission check** — every user may consume it                                        |

A denial closes the channel with `channel.close(403)` and RabbitMQ's own text:

```text
ACCESS_REFUSED - <verb> access to <queue|exchange> '<name>' in vhost '<vhost>' refused for user '<username>'
```

Both the connector check and platform authorization run **before** the operation is evaluated
for existence, so a denied caller cannot use the reply to learn whether a queue or exchange
exists.

### Rules to plan for [#rules-to-plan-for]

* **Patterns are an unanchored, case-sensitive search, as in RabbitMQ.** A pattern `q1` grants
  `q1`, `aq1b` and `q10`, never `Q1`. Lead every pattern with `^…$` unless the unanchored
  behavior is actually wanted. `""` grants **nothing** for that verb — an entry with every verb
  `""` denies everything in that vhost.
* **RE2, not PCRE.** A PCRE-only construct such as a negative lookahead (`^(?!amq\.).*`, a common
  way to deny the reserved `amq.` prefix) is refused by name at boot and on every save, rather
  than silently accepted and then never matching. Write the positive-list alternative instead:
  `^(orders|billing)\..*$` in place of a deny-`amq.`-prefix rule.
* **`Vhost: ""` is a wildcard matching any vhost — this connector's own extension.** RabbitMQ
  permissions are always scoped to one vhost, and a definitions export never produces a
  cross-vhost entry. An exact `Vhost` entry always wins over the wildcard for the same user.
* **The default vhost is always written `"/"`**, never its mapped `DefaultVhost` name — a client
  can only ever open it by naming `/`. An entry naming a vhost `connection.open` could never
  resolve (one containing `.`, a reserved channel character, or whitespace) is refused at boot.
* **A user with `Permissions` entries but none matching the vhost it opens is refused at
  `connection.open`:** `connection.close(530) NOT_ALLOWED - access to vhost '<vhost>' refused
  for user '<username>'`. Add a wildcard entry, or an entry naming that vhost. A user with **no**
  entries at all is never subject to this check — no connector-level check runs for it and
  platform authorization alone decides.
* **Topic permissions are not supported.** RabbitMQ's `set_topic_permissions` has no equivalent.
* **Platform-token logins keep the platform's two-verb model.** `Permissions` governs only a
  store user. A platform token, and `EXTERNAL` with no store configured, are governed by platform
  (Casbin) authorization alone, which has only `Read` and `Write`: there `queue.purge` and
  `queue.delete` both need `Write`, and a policy that lets a client declare a queue also lets it
  delete and publish to it.

## Tags [#tags]

`Tags` is RabbitMQ's own user-tag list (`rabbitmqctl set_user_tags`). The
[management HTTP API](/connectors/rabbitmq/reference/management-api) is the only consumer: any
one of `management`, `policymaker`, `monitoring` or `administrator` admits a store user to that
listener at all, and `administrator` additionally admits `GET`/`POST /api/definitions` and
widens visibility to every vhost. Any other value is stored and ignored, so a RabbitMQ
`definitions.json` export's tags round-trip through import without being rejected. Tags play no
part on the AMQP wire itself.

## Which subject a policy must name [#which-subject-a-policy-must-name]

When platform authorization is enabled, the subject it sees depends on how the connection
authenticated — and the credential store is consulted first:

| Derivation           | Selected when                                           | Subject                                                              |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------- |
| **Credential store** | `Connectors.Amqp.Credentials` has at least one entry    | `amqp-_user-` + the **encoded** username                             |
| **SASL `EXTERNAL`**  | `SslCertLogin = true` and a verified client certificate | `amqp-_cert-` + the **encoded** certificate identity                 |
| **Platform token**   | no store and `Authentication.Enable = true`             | the token's client id, verbatim and unprefixed                       |
| **Anonymous**        | no store and `Authentication.Enable = false`            | `amqp-` + 8 random characters per connection — no policy can name it |

The encoder keeps `a-z A-Z 0-9 -` unchanged, doubles `_`, and writes every other byte as `_x`
plus two lowercase hex digits:

| Configured `Username` | The `ClientID` pattern that matches it  |
| --------------------- | --------------------------------------- |
| `orders-producer`     | `^amqp-_user-orders-producer$`          |
| `orders_producer`     | `^amqp-_user-orders__producer$`         |
| `svc.orders`          | `^amqp-_user-svc_x2eorders$`            |
| `alice@corp.example`  | `^amqp-_user-alice_x40corp_x2eexample$` |

A policy naming the raw username matches nothing, and that user is then denied every operation.
Do not sanity-check the rule with a name like `alice` — it passes through unchanged and teaches
the wrong rule. Over-long names carry a digest instead of the full body; read the subject out of
the audit log's `auth.success` event rather than deriving it by hand.

<Callout type="warn">
  **Never write `^amqp-.*$` as a policy subject.** Every configured user and every certificate
  identity shares that namespace, so one such rule grants them all everything — and with no store
  and authentication off it also matches every anonymous connection. Write one anchored, encoded
  literal per user.
</Callout>

## Refreshing and expiring credentials [#refreshing-and-expiring-credentials]

A connection authenticated with a **platform token** is closed at the token's `exp` with
`connection.close(320)` `CONNECTION_FORCED - credential expired`, and can refresh itself in place
with `connection.update-secret` — see [Authentication](/connectors/rabbitmq/how-to/authentication).
A connection authenticated against **this store** holds no token: it is never evicted for age,
and `connection.update-secret` is refused `530` `NOT_ALLOWED - Secret update failed` whatever the
new secret says. Changing a user's password in configuration takes effect for **new** logins on
the next settings save or restart; existing connections are not re-authenticated.

## Kubernetes and the operator [#kubernetes-and-the-operator]

<Callout type="warn">
  **On an operator-managed Kubernetes cluster there is no shipped way to set this store today.**
  `Connectors.Amqp.Credentials` has no environment variable for the operator's
  `spec.envFromSecrets` to supply, and the settings page refuses every save on a server the
  operator manages. Plan such a cluster's `PLAIN`/`AMQPLAIN` clients around the paths that need
  no store: a platform token as the password when authentication is on, and any credentials when
  it is off. Certificate login over `EXTERNAL` needs no store either — `spec.tls` supplies the
  certificate, key and CA that mutual TLS needs, and `spec.env` sets
  `CONNECTORS_AMQP_SSL_CERT_LOGIN` — but that CA turns on mutual TLS for **every** TLS listener on
  the server, not only AMQP.
</Callout>

**Planned for v1.1.0 — not shipped.** A Kubernetes path is planned: a bootstrap user supplied
through `CONNECTORS_AMQP_DEFAULT_USER` and `CONNECTORS_AMQP_DEFAULT_PASS`, and a definitions file
loaded at startup through `CONNECTORS_AMQP_LOAD_DEFINITIONS`, both settable from
`spec.envFromSecrets`. Until it ships, none of those variables does anything.

## Related [#related]

<Cards>
  <Card title="Authentication" href="/connectors/rabbitmq/how-to/authentication" description="The four login paths, the JWT-in-password rule, certificate login, and token expiry and refresh." />

  <Card title="Management HTTP API" href="/connectors/rabbitmq/reference/management-api" description="The listener that consumes tags — endpoints, visibility scoping, and definitions import." />

  <Card title="Migrating from RabbitMQ" href="/connectors/rabbitmq/reference/migration-from-rabbitmq" description="The policy examples for both identity modes and the definitions export/import recipe." />

  <Card title="Configuration reference" href="/connectors/rabbitmq/reference/configuration" description="The Credentials field shape, validation rules, and every other Connectors.Amqp.* setting." />
</Cards>
