What should MQTT plugin behavior be for existing consumers be when their JWT token expires? #11854
-
Describe the bugI created a PahoMQTT Python MQTT consumer. The broker is not issuing any DISCONNECT messages, on top of that the consumer with expired JWT is getting all the messages. This is a very critical issue and a security vulnerability. I don't know if this issue is from the broker, or if it's from any of my configurations. Please help me to resolve the issue as I think it is a major drawback. Reproduction steps
Expected behaviorThe broker should not send any new message to the client and also should send a DISCONNECT message when the JWT expires. Additional contextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 8 comments 7 replies
-
Hello, thanks for using RabbitMQ. This issue report is woefully inadequate - you haven't even mentioned the versions of the software you are using. If you would like us to assist you, you must do the following:
|
Beta Was this translation helpful? Give feedback.
-
@arkadipdigite if this is a true security vulnerability, you should have reported the details in private but you've decided to start a public discussion with bold claims and zero evidence. Not cool. When a JWT token expires, connections will not be closed. However, individual operations such as publishing, registration of consumers ( New connections with an expired JWT token will be refused (there is a dedicated integration test for that). This is true for all protocols, not just MQTT. The OAuth 2 plugin will also try to renew the token (when it has a refresh token to do so) shortly before its expiration, by design. If that fails, eventually in many cases the connection will be closed, here are more integration tests that prove this claim. None of the protocol specs explicitly state what should happen when a JWT token expires. They simply do not cover JWT at all. For RabbitMQ's OAuth 2 plugin it means that all new operations performed by clients will be refused, including new subscriptions. It's a good question as to what should happen to existing connections, and yes, there can be multiple opinions about it. Finally, there is an emergecy "fix" that should work equally for all protocols: deleting a user will always close all of its connections. |
Beta Was this translation helpful? Give feedback.
-
@ansd has mentioned it to me in our team's chat that the MQTT plugin expires its permission cache after 1 second of client inactivity. @arkadipdigite so your bold claim of a security vulnerability comes down to a very specific set of circumstances that are present in every protocol implementation in RabbitMQ for efficiency reasons:
So the treatment of expired JWT tokens in RabbitMQ 3.13 (and in earlier versions) is fairly reasonable and does not cut corners but it also does not ruin the hot message delivery path throughput by checking for token validity after every message delivered. The only solution I can think of is introducing a periodic timer that would close connections with expired and non-renewed tokens. For environments with 100s of thousands or millions of connections it will come with a resource footprint price in terms of both per-connection RAM usage and CPU (context switching). The higher is the interval, the more resources will be wasted 99% (if not 99.9%) of the time (because most tokens are reasonably long lived, e.g. at least a few hours), and proportionally more so the more connections a cluster has. Update. @ansd has exactly this consideration in mind during the MQTT plugin redesign for 3.13: how not to waste resources 99% of the time by expiring the permissions cache too often or even for every message. |
Beta Was this translation helpful? Give feedback.
-
A set of relevant changes in the RabbitMQ Stream protocol: #10299. Note that with streams, there usually aren't millions of connections and we fully control the protocol and clients, which is not the case of MQTT or AMQP 1.0, so certain solutions for streams won't be feasible for other protocols. |
Beta Was this translation helpful? Give feedback.
-
Here is the running MQTT code import logging
import certifi
import paho.mqtt.client as mqtt
from paho.mqtt.enums import MQTTProtocolVersion
logging.basicConfig(level=logging.DEBUG)
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, reason_code, properties):
print(f"Connected with result code {reason_code}")
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("std/request/356303488760727")
# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
print(msg.topic+" "+str(msg.payload))
mqttc = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id="356303488760727",
protocol=MQTTProtocolVersion.MQTTv5,
)
mqttc.username_pw_set('', '<JWT TOKEN>')
mqttc.tls_set(
ca_certs=certifi.where()
)
mqttc.on_connect = on_connect
mqttc.on_message = on_message
mqttc.connect("broker.url.com", 8883, 60)
# Blocking call that processes network traffic, dispatches callbacks and
# handles reconnecting.
# Other loop*() functions are available that give a threaded interface and a
# manual interface.
mqttc.loop_forever() Here are the version details ## ## RabbitMQ 3.13.2
## ##
########## Copyright (c) 2007-2024 Broadcom Inc and/or its subsidiaries
###### ##
########## Licensed under the MPL 2.0. Website: https://rabbitmq.com
Erlang: 26.2.5 [jit]
TLS Library: OpenSSL - OpenSSL 3.1.5 30 Jan 2024 Python Version: RabbitMQ Configuration apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: rabbitmq
namespace: rabbitmq
spec:
replicas: 1 # Number of replica
terminationGracePeriodSeconds: 60 # Termination Grace Period Timeout
service:
type: ClusterIP
# TLS certificate
tls:
secretName: broker-url-com-tls
disableNonTLSListeners: false
# Persistence Storage
persistence:
storageClassName: managed-premium-retain
storage: 32Gi
# Resource request and limits
resources:
requests:
cpu: 100m
memory: 500Mi
limits:
cpu: 1000m
memory: 2Gi
# rabbitmq config
rabbitmq:
# Additional Config
additionalConfig: |
log.default.level = debug
default_user = admin
default_pass = Password
auth_backends.1 = rabbit_auth_backend_oauth2
auth_oauth2.resource_server_id = rabbitmq
auth_oauth2.additional_scopes_key = permissions
management.oauth_disable_basic_auth = false
advancedConfig: |
[
{rabbitmq_auth_backend_oauth2, [
{key_config, [
{default_key, <<"id1">>},
{signing_keys,
#{<<"id1">> => {pem, <<"-----BEGIN PUBLIC KEY-----
Key Goes Here
-----END PUBLIC KEY-----">>}
}
}
]}
]}
].
# Env config
# envConfig: |
# RABBITMQ_DISTRIBUTION_BUFFER_SIZE=some_value
# Additional RabbitMQ plugins
additionalPlugins:
- rabbitmq_management
- rabbitmq_mqtt
- rabbitmq_event_exchange
- rabbitmq_auth_backend_oauth2 |
Beta Was this translation helpful? Give feedback.
-
This discussion is going in circles. We have provided an explanation of
RabbitMQ's OAuth 2 and MQTT implementations do not cut corners and the only scenario where an expired JWT token would not very quickly result in client disconnection has been considered, weighted against its throughput and resource consumption downsides, and left as is. If someone has a specific design in mind that has minimal resource and throughput effects, they are welcome to start a new discussion or submit a PR. |
Beta Was this translation helpful? Give feedback.
-
#11862 proposes a specific (and hardly elegant) solution that would neither affect the throughput nor will introduce an additional per-connection timer. This is not a promise of delivery: the above issue may or may not be investigated, or work out well, or ship in any specific release. |
Beta Was this translation helpful? Give feedback.
-
#11867 fixes this issue. |
Beta Was this translation helpful? Give feedback.
@arkadipdigite how is that "not 100% accurate"? You were provided a detailed description of the behavior and the design decision by the core team. If that's not accurate enough for you, go read the source code.
It was explained above that this means using a timer per connection, and with 100s of thousands or
millions of connections will be a non-trivial waste of resources, in particular CPU resources but also memory, and reducing per-connection memory consumption was the key design goal of the MQTT plugin redesign in 3.13.
@arkadipdigite …