0%
RabbitMQ Deep Dive: AMQP, Queues, and Delivery Guarantees
Queues and Storage
Delivery Guarantees
Flow and Scale
Patterns
Routing in Practice: Permissions, Federation and Tracing
RabbitMQ's authorisation model is three regular expressions per user per vhost, and the reason it belongs in a routing lesson rather than a security one is that the three verbs map onto protocol operations in a way nobody guesses correctly, and a permission mistake presents as a routing bug.
The three verbs
Configure, write and read, applied to resource names.
rabbitmqctl set_permissions -p <vhost> <user> <configure> <write> <read> takes three regular expressions, each matched against the name of the exchange or queue an operation touches. There is no distinction in the regex between an exchange and a queue, which is the first thing to design around: a pattern of ^orders\. grants over both an exchange and a queue with that prefix.
Configure gates declaration and deletion. exchange.declare, exchange.delete, queue.declare and queue.delete require configure on the named resource, and nothing else does.
Write gates publishing, so basic.publish requires write on the exchange it names. Note which object that is: publishing requires no permission on the queue the message lands in, because the publisher does not name one.
Read gates consuming, so basic.get, basic.consume and queue.purge require read on the queue.
Binding needs two permissions, and they are the surprising ones
The rule that produces most 403s.
queue.bind and queue.unbind require write on the queue and read on the exchange. That is backwards from the intuition that binding is a configuration operation, and it is deliberate: the model treats a binding as establishing a flow, so you need permission to put messages into the destination and permission to take them out of the source.
exchange.bind and exchange.unbind follow the same rule with both ends being exchanges: write on the destination exchange and read on the source.
The practical consequence is that a user granted configure and write but no read cannot bind anything, and the error, 403 ACCESS_REFUSED, names the exchange rather than the operation, which sends people looking at publish permissions. The second consequence is that the standard "publisher" role, write only, cannot declare the topology it publishes into, which is usually correct and is usually discovered at the wrong moment.
Designing the three roles
Publisher, consumer, operator, and the regexes that express them.
A publisher needs write on the exchanges it publishes to and nothing else: configure ^$, write ^(orders|payments)$, read ^$. The empty pattern ^$ matches no name at all, which is how a permission is denied, and using .* as a placeholder because the empty pattern looks wrong is how a publisher ends up able to delete queues.
A consumer needs read on its queues, and write on them too if it declares its own bindings: configure ^$, write ^svc-billing\., read ^svc-billing\.|^orders$. The ^orders$ in read is the exchange it binds from, which is the part that looks like a mistake and is not.
An operator, meaning the deployment job that applies the topology, needs configure, write and read over everything it manages, which is the one account that legitimately holds .* and the one that should not be used by any running service.
Writing all three out per service is tedious and is exactly what a definitions file is for. The check worth running once is rabbitmqctl list_permissions -p <vhost>, because the management UI's convenience button grants .* for all three verbs and estates accumulate those.
The verb-to-operation table, written out
Because every team eventually rediscovers it from 403s.
Three roles, three lines, and the one that looks wrong in review is the middle one: a consumer needs read on the exchange because that is what queue.bind requires of the source. The publisher's two empty patterns are not placeholders waiting to be filled in; they are the denials, and replacing either with a wildcard because the empty pattern looks unfinished is the single most common way a publisher ends up able to delete the queues it publishes into.
How the check is evaluated, and what it costs
Per operation, with a cache, and the cache is why a revocation is not immediate.
A permission check compiles the three patterns and matches them against the resource name, and the result is cached per connection and resource for the life of the connection. That is what makes basic.publish affordable: the regex does not run per message, it runs the first time that channel publishes to that exchange.
The operational consequence is that revoking a permission does not stop an established connection. clear_permissions or a narrowed pattern affects the next check on a resource the connection has not touched, and a publisher that has been writing to one exchange for six days continues writing to it. Closing the connection, with rabbitmqctl close_connection or by deleting the user entirely, is what actually revokes access, and a security runbook that stops at set_permissions has not finished.
Topic permissions are the exception, and the reason is that the thing being matched is the routing key rather than the resource name, so it changes per message and cannot be cached per resource. That makes a topic permission a genuine per-publish regex evaluation, which is cheap for an anchored prefix pattern and measurable for a pattern with alternation and backtracking.
Topic permissions, which gate routing keys
A fourth regex, and it is the only authorisation on a routing key.
rabbitmqctl set_topic_permissions -p <vhost> <user> <exchange> <write-pattern> <read-pattern> adds a regular expression matched against the routing key, for topic exchanges only. Write gates which keys a user may publish, read gates which keys they may bind to.
The patterns support variable expansion: {username}, {vhost} and {client_id}, the last for MQTT connections. So a pattern of ^{username}\. gives every user a private prefix of the key space, enforced by the broker, which is the only mechanism in RabbitMQ that prevents one tenant publishing into another tenant's routing namespace.
The limitations are worth knowing before relying on it. It applies to topic exchanges only, so a direct or fanout exchange has no key-level authorisation at all. Absence of a topic permission means allow, so the feature is opt-in per user and exchange rather than a default-deny. And the regex runs on every publish, so a pathological pattern is a per-message cost.
Why a permissions error reads like a routing error
The failure modes, and how to tell them apart quickly.
A missing write on an exchange fails the publish loudly, with 403 and a closed channel, which is the easy case.
A missing read on the exchange fails the bind, and if the application catches the channel exception and carries on, the binding never exists and every subsequent publish routes nowhere and succeeds. That is a permissions bug presenting as the classic silent-discard, and it is indistinguishable from a routing key typo without looking at the binding table.
A missing topic permission fails the publish with 403, which is loud, but the pattern being absent for one exchange and present for another produces a service that works for some keys and not others, which reads as a routing schema problem.
The three-command triage is rabbitmqctl list_permissions, list_topic_permissions and list_bindings, in that order, and running all three takes under a minute. The reason to do it first rather than last is that a routing investigation that starts from the binding table cannot distinguish "never created" from "created and then deleted by a failed redeclare", and the permissions tables can.
The defaults that ship, and the two that matter
One user, one vhost, and a restriction that is doing more work than people realise.
A fresh broker has the vhost / and the user guest with the password guest and full permissions over it. The reason this is not immediately a disaster is loopback_users, a configuration setting defaulting to a list containing guest, which restricts that user to connections from localhost. A remote guest login fails with 403 regardless of the password being correct, which is the single most common "my credentials work locally and not in staging" report.
The two implications are worth separating. First, the protection is a network restriction rather than a credential one, so a guest login succeeds from anything that reaches the broker over loopback, which includes a sidecar container, a process on the same host and anything that has arranged a tunnel. Deleting the user is the actual fix and takes one command. Second, removing guest from loopback_users to make a remote test work, which is a documented setting and therefore feels sanctioned, converts a local-only account with full permissions into a fully exposed one.
Alongside permissions there are limits, which are a separate mechanism and are the only thing in the model that bounds resource use rather than access. rabbitmqctl set_user_limits <user> '{"max-connections":100,"max-channels":2000}' and set_vhost_limits -p <vhost> '{"max-connections":500,"max-queues":1000}' cap what a user or vhost can hold. A max-queues limit is the one that matters most in a multi-tenant estate, because queue count sizes the Erlang VM and a tenant creating a queue per user can degrade a node every other tenant shares. Limits are not permissions and are not in the three regexes, so a permission audit that does not look at them has checked who can do what and not how much.
queue.bind requires write on the queue and read on the exchange, not configure, which is backwards from the intuition that binding is configuration. A service granted configure and write but no read cannot bind anything, and if it catches the 403 and continues, the binding silently never exists and every later publish routes nowhere and reports success. That is a permissions bug wearing the costume of a routing key typo, which is why list_permissions belongs at the start of a missing-messages investigation rather than the end.