EMQX Enterprise 6.3.0 focuses on stronger security defaults, leaner deployments, and broader operational visibility.
Highlights include:
- Feature Gates. Set
EMQX_FEATURES=ESSENTIALto run the core MQTT broker with an under-100 MB boot-time memory footprint; - Security Profile. Set
EMQX_SECURITY_PROFILE=hardenedfor secure-by-default deployments; - A more performant socket backend for MQTT TCP listeners;
- Refined QoS 0 flow control for slow or congested subscribers;
- Topic Metrics v2 with wildcard matching and Prometheus scraping;
- Authenticated OpenAPI and Prometheus endpoints;
- Stronger credential hashing;
- Improved MQTT session handling and rate limiting;
- Bigtable integration.
In summary, this release includes 39 enhancements and 47 bug fixes comparing to version 6.2.3
Download
Ubuntu / Debian
| OS | Arch | Package | Tarball |
|---|---|---|---|
ubuntu24.04
| amd64 | .deb (sha256) | .tar.gz (sha256) |
ubuntu24.04
| arm64 | .deb (sha256) | .tar.gz (sha256) |
ubuntu22.04
| amd64 | .deb (sha256) | .tar.gz (sha256) |
ubuntu22.04
| arm64 | .deb (sha256) | .tar.gz (sha256) |
debian13
| amd64 | .deb (sha256) | .tar.gz (sha256) |
debian13
| arm64 | .deb (sha256) | .tar.gz (sha256) |
debian12
| amd64 | .deb (sha256) | .tar.gz (sha256) |
debian12
| arm64 | .deb (sha256) | .tar.gz (sha256) |
debian11
| amd64 | .deb (sha256) | .tar.gz (sha256) |
debian11
| arm64 | .deb (sha256) | .tar.gz (sha256) |
RHEL / Rocky / Amazon Linux
| OS | Arch | Package | Tarball |
|---|---|---|---|
el10
| amd64 | .rpm (sha256) | .tar.gz (sha256) |
el10
| arm64 | .rpm (sha256) | .tar.gz (sha256) |
el9
| amd64 | .rpm (sha256) | .tar.gz (sha256) |
el9
| arm64 | .rpm (sha256) | .tar.gz (sha256) |
el8
| amd64 | .rpm (sha256) | .tar.gz (sha256) |
el8
| arm64 | .rpm (sha256) | .tar.gz (sha256) |
amzn2023
| amd64 | .rpm (sha256) | .tar.gz (sha256) |
amzn2023
| arm64 | .rpm (sha256) | .tar.gz (sha256) |
el7
| amd64 | .rpm (sha256) | .tar.gz (sha256) |
macOS
| OS | Arch | Package |
|---|---|---|
macos14
| arm64 | .zip (sha256) |
macos15
| arm64 | .zip (sha256) |
macos26
| arm64 | .zip (sha256) |
Plugins
| Plugin | Version | Package |
|---|---|---|
emqx_acme
| 0.2.0 | .tar.gz (sha256) |
emqx_agent
| 1.0.0 | .tar.gz (sha256) |
emqx_backup_sync
| 0.1.3 | .tar.gz (sha256) |
emqx_bridge_mqtt_dq
| 0.5.2 | .tar.gz (sha256) |
emqx_maptabs
| 0.1.2 | .tar.gz (sha256) |
emqx_offline_messages
| 2.0.1 | .tar.gz (sha256) |
emqx_relup
| 1.0.2 | .tar.gz (sha256) |
emqx_sync_request
| 0.1.1 | .tar.gz (sha256) |
emqx_unsgov
| 0.1.4 | .tar.gz (sha256) |
emqx_username_quota
| 1.2.3 | .tar.gz (sha256) |
Breaking Changes
-
#17185 The MQTT parser now runs in strict mode by default. To restore the previous lenient behavior, set
mqtt.strict_mode = false(globally or per-zone).In strict mode, the broker validates incoming MQTT packets against the protocol specification and disconnects clients that send malformed packets. The validations enforced only in strict mode are:
- Fixed-header flags. Reserved DUP/QoS/RETAIN bits must be zero for non-PUBLISH packets, and PUBREL/SUBSCRIBE/UNSUBSCRIBE must use QoS=1 (
bad_frame_header). - CONNECT reserved bit must be zero (
reserved_connect_flag). - CONNECT Will flag consistency: Will Flag=0 requires Will QoS=0 and Will Retain=0; Will Flag=1 requires Will QoS in {0,1,2} (
invalid_will_qos,invalid_will_retain). - CONNECT Password/Username flags (MQTT 3.1.1 only). If Username Flag=0, Password Flag must also be 0, per
[MQTT-3.1.2-22](invalid_password_flag). MQTT 5.0 lifts this constraint and is unaffected. - UTF-8 strings (proto name, client ID, topic, username, password, will topic, MQTT 5 string properties) must be valid UTF-8 and must not contain control characters U+0000–U+001F or U+007F–U+009F (
utf8_string_invalid). - Packet identifiers must be non-zero where required (PUBLISH QoS>0, PUBACK/REC/REL/COMP, SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK) (
bad_packet_id).
When a client violates one of these checks, the broker logs an
info-level entry withmsg=frame_parse_errorand a structuredreason(for example,cause=invalid_password_flag,proto_ver, orreceived_prefix) for troubleshooting. For MQTT 5.0 connections, the broker also responds with CONNACK/DISCONNECT carrying reason code0x81 Malformed Packetbefore closing; for MQTT 3.1/3.1.1, the connection is silently closed (no CONNACK reason code is defined for malformed packets in those versions). - Fixed-header flags. Reserved DUP/QoS/RETAIN bits must be zero for non-PUBLISH packets, and PUBREL/SUBSCRIBE/UNSUBSCRIBE must use QoS=1 (
-
#17215 Removed the bundled Swagger UI assets from the EMQX release package, reducing tarball size by approximately 11 MB.
/api-docs/swagger.jsoncontinues to serve the full OpenAPI 3 JSON spec, so external Swagger UI deployments that load it by URL keep working. The legacy/api-docsURL responds with an HTTP 308 redirect to/api-spec.html, the in-tree spec explorer introduced in 6.3.0. Other/api-docs/*subpaths (the embedded Swagger UI assets) are no longer served and return 404. -
#17267 The
node.max_portsconfig now defaults toauto, which scales the Erlang VM port limit (+Q) with the number of logical CPU cores: 65536 ports per core for up to 8 cores, and 1048576 (the historical fixed default) above that. Explicit integer values are still accepted.This is a behavior change for nodes upgraded from earlier versions where
max_portsdefaulted to a fixed 1048576: hosts with 8 or fewer CPU cores will now boot with a smaller port table. Setups that rely on accepting more thancores * 65536connections must setnode.max_portsexplicitly (and restart the node) before upgrading.The hidden
node.process_limitsetting is reinstated as an override: when set to a value larger than the derived limit (2 * max_ports), it is respected; smaller values are ignored so the process table never under-sizes the port table.A new
node.schedulerssetting (defaultauto) controls the Erlang scheduler count (+S). Withauto, the count is capped at the number of logical processors actually available to the VM (sched_getaffinityon Linux), so containers limited via--cpuset-cpusor Kubernetes CPU requests no longer spawn scheduler OS threads they cannot run in parallel. Set it to a positive integer to override the auto-detected value. -
#17437 Prometheus scrape endpoints (
/api/v5/prometheus/*) now require authentication by default. Setprometheus.enable_basic_auth = falseexplicitly to restore the previous unauthenticated behavior. Deployments that scrape these endpoints without credentials will need to either configure credentials on the scraper or set the config field. The recommended setup is a dedicated API key with themonitoringscope, used with Bearer auth in the scraper. -
#17582 Prometheus VM and Mnesia collector metric names now use the
prometheus.erl6.x promtool-compliant names.Affected metric renames:
erlang_mnesia_failed_transactions->erlang_mnesia_failed_transactions_totalerlang_mnesia_committed_transactions->erlang_mnesia_committed_transactions_totalerlang_mnesia_logged_transactions->erlang_mnesia_logged_transactions_totalerlang_mnesia_restarted_transactions->erlang_mnesia_restarted_transactions_totalerlang_vm_memory_atom_bytes_total->erlang_vm_memory_atom_byteserlang_vm_memory_bytes_total->erlang_vm_memory_byteserlang_vm_memory_processes_bytes_total->erlang_vm_memory_processes_byteserlang_vm_memory_system_bytes_total->erlang_vm_memory_system_byteserlang_vm_statistics_context_switches->erlang_vm_statistics_context_switches_totalerlang_vm_statistics_garbage_collection_number_of_gcs->erlang_vm_statistics_garbage_collection_number_of_gcs_totalerlang_vm_statistics_garbage_collection_words_reclaimed->erlang_vm_statistics_garbage_collection_words_reclaimed_totalerlang_vm_statistics_garbage_collection_bytes_reclaimed->erlang_vm_statistics_garbage_collection_bytes_reclaimed_totalerlang_vm_statistics_runtime_milliseconds->erlang_vm_statistics_runtime_seconds_totalerlang_vm_statistics_wallclock_time_milliseconds->erlang_vm_statistics_wallclock_time_seconds_totalerlang_vm_port_count->erlang_vm_portserlang_vm_process_count->erlang_vm_processeserlang_vm_atom_count->erlang_vm_atoms
-
#17596 Added authorization options that forbid interpolation of
/,+, and#symbols into topic filter templates in authorization rules. The new options are:authorization.topic_template_allow { plus = false, hash = false, slash = false }
With
false, the corresponding symbol cannot be used in a value interpolated into a topic template. For example, ifplus = false, then usernamebad+useris forbidden in a rule such as{allow, all, publish, ["userspace/${username}"]}. The outcome depends on the active security profile: with the legacy profile the rule will not match, and with the hardened profile the action will be denied. -
#17677 Dropped support for the JSON output format in the Prometheus REST API.
The endpoints under
/api/v5/prometheus(stats,auth,data_integration,schema_validation,message_transformation) now only produce the Prometheus text format. Requests sendingAccept: application/jsonare rejected with400 Bad Request("only prometheus format is supported"); previously they returned a JSON representation of the metrics. -
#17626 #18123 Added a new configuration
multi_tenancy.deny_namespacesholding namespace names that cannot be used as a namespace identifier, either as an admin namespace (dashboard roles, API keys, multi-tenancy management API) or as a per-clientclient_attrs.tns; a client whoseclient_attrs.tnsresolves to a denied name is rejected.This is a breaking change: the default value
["global", "undefined", "null", "none"]denies names that were previously accepted. These names collide with internal sentinels and would produce ambiguous log lines and dashboard output. Existing namespaces with these names are not migrated; rename them before upgrading, or setmulti_tenancy.deny_namespacesto an empty list to lift the restriction.Additionally, when
multi_tenancy.post_auth_tns_expressionis configured and evaluates to an empty value or fails to evaluate, a client whose pre-authenticationclient_attrs.tnsis a denied namespace name is now also rejected, consistent with the handling when the expression evaluates to a non-empty value. -
#18228 The default authorization rules file (
acl.conf) no longer grants clients connecting from127.0.0.1blanket publish/subscribe access to all topics (including$SYS/#and#).Clients connecting from localhost are now authorized by the same rules as any other client, and ultimately by the
authorization.no_matchsetting. In particular, subscriptions to$SYS/#and the wildcard filters#and+/#are now denied for localhost clients by the default rules, regardless of the security profile.Deployments that relied on the built-in localhost allowance must add an explicit rule to
acl.conf. The previous rule is retained in the file as a comment for easy re-enabling:%% {allow, {ipaddr, "127.0.0.1"}, all, ["$SYS/#", "#"]}.Note: this applies to new installations and deployments that have not customized
acl.conf; existing customizedacl.conffiles are not modified by upgrades. -
#18244 The ExProto gateway has been removed.
-
#18271 #18329 MQTT and gateway WebSocket listeners no longer read the client address and port from forwarded headers by default: the default value of
proxy_address_headerandproxy_port_headerchanged fromx-forwarded-for/x-forwarded-portto empty, meaning the socket source address and port are always used. Deployments behind load balancers or reverse proxies that rely on forwarded headers must now configure the header names explicitly (for example, setproxy_address_headertox-forwarded-for). Setting an empty header name disables the forwarded-header lookup.This change also fixes the forwarded-header lookup for gateway WebSocket listeners. Previously, a configured header name was never matched against the request headers, so the socket source address and port were used even when the forwarded headers were present.
-
#18377 Managed namespace names are now validated when created. A name may contain only ASCII letters, digits, and the characters
.,-, and_, with a length of 1 to 255 bytes; the names.and..are not accepted. Namespaces that already exist are not affected. -
#18390 The
mqtt.clientid_overrideexpression no longer falls back to the client-supplied Client ID when it fails.When
mqtt.clientid_overrideis configured and the expression raises an error (for example, it references an attribute the client did not provide) or renders an empty string, EMQX now refuses the connection with CONNACK reason code 0x85 (Client Identifier not valid; return code 2 for MQTT 3.1 and 3.1.1 clients). Previously such clients stayed connected under their original Client ID, so the override silently did not apply to them.Before upgrading, verify that every connecting client can render the configured expression to a non-empty string. Clients that could not render the expression connected with their original Client ID before the upgrade; after the upgrade they are refused until the expression or the client data is fixed.
-
#18419 Removed the Google Cloud IoT Core migration compatibility feature, including the GCP Device authenticator and device management APIs.
-
#18515 Updated the Azure Blob Storage Action's
blobtemplate field to use the same schema validation as the Aggregated S3 Action'skeyfield. The validation rejects unsupported template bindings. -
#18528 Added validation that requires the exporter endpoint of an OpenTelemetry integration to be a URL with an explicit scheme and port. Supported schemes are
httpandhttps. -
#18627 Dashboard SAML SSO now verifies IdP signatures by default in all security profiles.
Previously the default followed the security profile: the hardened profile verified signatures, but the legacy profile (the default until v7.0) did not, so it accepted an unsigned, forged SAMLResponse and issued a Dashboard session.
If you intentionally run an unsigned IdP, set
sso.saml.idp_signs_envelopes = falseandsso.saml.idp_signs_assertions = falseexplicitly. If the IdP does sign but its metadata carries no certificate, the SAML backend now fails to start withmissing_idp_certificate.
Enhancements
Core MQTT Functionalities
-
#16694 Added support for extracting peer certificates from QUIC connections, enabling
peer_cert_as_usernameon QUIC mTLS listeners. -
#17307 Added a per-client rate limiter for SUBSCRIBE packets on listeners. It is disabled by default. When configured with a finite rate, EMQX responds to packets that exceed the limit with a SUBACK containing the Quota Exceeded reason code and does not process them. Namespaces can configure independent rates, which override the listener-level rate.
-
#17546 #18477 Added the
mqtt.max_session_expiry_intervalconfiguration to cap the session expiry interval an MQTT 5.0 client may request via itsSession-Expiry-Intervalproperty. If the client requests longer than this limit, the server silently clamps it and reflects the clamped value back in the CONNACK. The setting defaults toinfinity(no clamp), preserving the previous behavior. It has no effect on MQTT 3.1.1/3.1 clients, whose session expiry remains fully server-controlled viamqtt.session_expiry_interval.The cap also applies to the Session Expiry Interval a client supplies in a DISCONNECT packet, so a client cannot extend its session expiry beyond the configured limit when disconnecting.
-
#17603 Added support for extracting subject alternative names from directly connected TLS client certificates into MQTT client attributes with
cert_san.dns,cert_san.ip,cert_san.email, andcert_san.uriinmqtt.client_attrs_init. -
#17854 Changed the default
tcp_backendfor MQTT TCP listeners tosocketon Unix systems to improve message latency and resource usage. Thegen_tcpbackend remains available by settingtcp_backend = gen_tcp, and remains the default on Windows. -
#17870 Improved in-memory session delivery behavior for slow or congested subscribers.
- EMQX now tracks connection send-queue congestion and moves QoS 0 deliveries through the session message queue when needed, instead of continuing to push them directly to a congested connection.
- The session message queue now prefers evicting older QoS 0 messages when it reaches capacity, helping QoS 1 and QoS 2 deliveries make progress during QoS 0 bursts.
- Fixed delivery ordering when a session delivery rate limit is reached, so later messages do not overtake earlier queued messages.
- Improved socket-backed connection
send_timeouthandling so the timeout is armed only after the socket queue is past its watermark.
Access Control
-
#17145 Authorization sources now support Variform-based preconditions. A source with a precondition is called only when the expression evaluates to
true, allowing different authorization backends to be selected by client and request context such as client attributes, action, and topic. -
#17487 Strengthened Dashboard administrator password and API key secret hashing.
Dashboard administrator passwords are now hashed with PBKDF2-HMAC-SHA256 (600,000 iterations) and a 16-byte random salt. API key secrets are hashed in the same self-describing storage format with the same 16-byte random salt but without iteration stretching, so per-request HTTP API authentication remains cheap. The previous scheme (single-pass SHA-256 with a 2-byte salt) is replaced for both credential types.
Existing stored hashes continue to be accepted for authentication. They are rewritten in the new format the next time the user changes their password or the API key is recreated.
-
#17671 Authentication and authorization rejection logs now include per-backend attribution.
When EMQX rejects a client because an authenticator returns an error, it emits a warning log identifying the authenticator ID and provider that produced the rejection. When an authorization source denies an operation, EMQX logs the denial at warning level (previously only visible under a client trace) with the source type, module, topic, and action.
This makes it possible to tell which backend produced a decision in deployments with multiple authenticators or authorization sources, without having to enable a client trace first. The new logs are throttled per authenticator and per authorization source to avoid log floods.
-
#18130 Added support for template variables in the URL host of HTTP authenticators and authorizers, for example
https://${client_attrs.tns}.auth.example.com/authn, enabling per-tenant authentication and authorization endpoints without an external routing layer.The
hostname_resolutionsetting controls how the URL host is handled. The defaultstaticpreserves the previous behavior: a fixed hostname and a persistent connection pool established at configuration time. Setting it todynamic(required when the URL host contains template placeholders) makes EMQX resolve the hostname for every request and send each check over a per-request connection, applying the configured TLS options (including peer verification) to that host; thepool_sizesetting then limits how many idle connections may be kept for reuse (0disables reuse), while pipelining options do not apply.For templated hosts, the new
allowed_hostssetting must list the hostnames the host may render to, either as exact names or as*.suffixwildcard patterns; when the rendered hostname is not covered by this list, no request is made and the check fails. URLs with a literal hostname keep using the connection pool exactly as before. -
#18239 #18371 Flapping detection can now also act on the username and the source IP address of connecting clients, in addition to the client ID.
Each dimension has its own detection window, connect-attempt threshold, and ban duration, configured under
flapping_detect.by_clientid,flapping_detect.by_username, andflapping_detect.by_peerhost(username and source IP detection are disabled by default). The client ID dimension was previously configured with the flatflapping_detectfields (enable,window_time,max_count,ban_time); these are now deprecated but remain accepted and are automatically mapped ontoby_clientid, so existing configurations keep working unchanged. When a username or source IP address exceeds its threshold within the detection window, it is temporarily banned: new connection attempts are rejected before authentication runs, while already-connected clients are not affected. Ban entries expire automatically and can be inspected or removed early via the/bannedREST API; each entry carries the ban type (as:clientid,username, orpeerhost) andby=flapping detector, and the list can be filtered by type.Counters are kept separately per zone and per dimension, so zones configured with different detection windows do not discard each other's still-active counters.
New metrics:
flapping.detected.clientid,flapping.detected.username, andflapping.detected.peerhostcount flapping triggers per dimension, andclient.bannedcounts connection attempts rejected due to an active ban entry.
Multi-tenancy
- #17454 Scoped Prometheus data returned by
/api/v5/prometheus/data_integrationto the requesting actor's namespace. A global administrator can view data from all namespaces and filter by namespace.
Data Integration
-
#17129 Added Attached Service Account authentication for GCP-based connectors (GCP PubSub Producer, GCP PubSub Consumer, and BigQuery). When EMQX runs on a GCP VM with a service account attached to the instance, it can query the internal metadata endpoint to obtain a token for these connectors.
-
#17222 Added a Bigtable data integration that supports appending data to Bigtable.
-
#17547 Added support for AWS IAM Roles Anywhere in Kafka Producer and Consumer Connectors. The Connectors can be configured with the HTTP endpoint exposed by the credential helper process.
The credential helper process must be running and accessible to EMQX. See the AWS IAM Roles Anywhere credential helper documentation for more information.
-
#17783 Added an
application_nameoption to PostgreSQL-family connectors. It defaults toemqxand is sent as the PostgreSQL startup parameter so connector sessions can be identified in PostgreSQL activity views and logs. The value must be 1 to 63 bytes long and cannot contain zero bytes. -
#18119 The Disk Log connector now supports time-based file rotation in addition to size-based rotation.
A new optional
rotationsetting was added to the connector configuration:rotation.period:none(default),day, orhour. When set todayorhour, the connector starts a separate set of log files at each period boundary, with the period's date stamp (YYYYMMDDHH) encoded in the file names (for example,mqtt-trace-2026062400.log.1for daily rotation ofmqtt-trace.log). Size-based rotation (max_file_size/max_file_number) still applies within each period.rotation.retention_period: how long to keep files from previous periods (for example,30d); date-stamped files outside the retention period are deleted automatically after each period rotation. Defaults toinfinity(files are retained indefinitely).rotation.timezone: timezone used to determine period boundaries:UTC(default),local, or a fixed offset such as+02:00.
The default behavior (no
rotationconfigured, orrotation.period = none) is unchanged. -
#18319 Added support for specifying an action-specific project ID for BigQuery Actions.
-
#18624 Added
emqx ctl actions showandemqx ctl actions statuscommands. They report action status for the local node only, in JSON, without REST API credentials or a network call.statusprints a compact JSON array of{"<type>:<name>": "<status>"}entries;showprints the same information asGET /api/v5/actions/{id}, with connector secrets redacted, but for the local node only. Both accept--name <type:name>to select one action and--ns <namespace>to select a namespace, defaulting to every action in the global namespace.This suits a per-node readiness probe, where the REST API's cluster-aggregated
statusfield cannot tell whether the local node's own actions are ready to accept traffic.
Observability
-
#17493 Added session buffer observability: client APIs now report
total_payload_bytes,sysmon.session.total_payload_bytes_high_watermarkcan emit throttled warnings for sessions over a payload-byte threshold, andemqx ctl session-topcan export the cluster top sessions bytotal_payload_bytesormqueue_length. -
#17582 Updated Prometheus metrics collection to use
prometheus.erlversion 6.1.2, improving performance and scalability.The
vm_dist,vm_statistics,vm_system_info, andvm_memorycollectors are now enabled by default. The obsoleteemqx_vm_process_messages_in_queuesmetric was removed from Prometheus output. -
#17607 #17998 Added a v2 topic-metrics surface with named collections, wildcard topic filters, namespace ownership, REST CRUD and a Prometheus scrape endpoint.
- New routes under
/api/v5/mqtt/topic_metrics2/:namelet operators register topic-metric collections by user-chosen name (my-pressure,vehicle-events, …) instead of using the topic itself as the identifier. Wildcards (t/#,sensor/+/temp) are now accepted as topic filters, and a single published message may match and increment several collections. - Collections are namespace-scoped: a collection created by a namespaced admin only counts publishers whose
client_attrs.tnsmatches. Global collections (created by a non-namespaced admin) count every publisher. Namespaced admins see and modify only their own collections. A global administrator can address an individual namespaced collection on the per-collection endpoints (GET,DELETE,PUT .../reset) by passing annsquery parameter; a namespaced admin passing another namespace's name is rejected with403 Forbidden, and omittingnskeeps the actor's own namespace. - Counters are exposed in Prometheus exposition format at
/api/v5/prometheus/topic_metricswith labelsname,topic_filter,namespace. Rates can be derived via Prometheusrate(). - The v1 API (
/api/v5/mqtt/topic_metricsand/api/v5/mqtt/topic_metrics/:topic) is unchanged and continues to work; it is marked deprecated in its Swagger spec, and integrations should use v2.
- New routes under
-
#18148 Added support for Dynatrace integration via OpenTelemetry. Supported signals are traces and logs. The integration uses OAuth2 tokens for authentication.
Deployment and Security
-
#17381 The OpenAPI specification endpoints now require authentication by default. This covers
GET /api-docs/swagger.json,GET /api-spec.json,GET /api-spec.md, andGET /api-spec/:tag[/:name].Unauthenticated requests receive a 401 with a
WWW-Authenticateheader and a minimal but valid OpenAPI document (or its Markdown equivalent for/api-spec.md) that lists the supported security schemes and the public bootstrap endpoints (POST /api/v5/login,GET /api/v5/status), so callers can discover how to authenticate without the dashboard exposing its full API surface anonymously.The dashboard's
api-spec.htmlexplorer continues to load anonymously and fetches the spec with the existing session cookie or token. -
#17407 #17808 Added Feature Gates.
Added support for starting EMQX with a limited set of features specified by the
EMQX_FEATURESenvironment variable. Invalid presets or feature names prevent the node from booting. Dependent features are enabled automatically.There are two presets available:
FULL: the default. Starts EMQX with all available features.ESSENTIAL: starts EMQX with the minimum feature set: the core MQTT broker with authentication and authorization.
The available features are:
dashboard: Dashboard UI (including SSO and RBAC), REST API.data_integration: Connectors, Actions, Sources, and Rule Engine.message_transformation: Message transformation.schema_validation: Schema validation.schema_registry: Schema registry.gateways: Gateway protocols.cluster_link: Cluster linking.multi_tenancy: Multi-tenancy and namespacing.ai: AI features (A2A registry, AI completion).metrics: Prometheus metrics exporting.mqtt_extensions: MQTT extensions: delayed publish, topic rewrite, Auto Subscribe, Slow Subscriptions, message queue, and streams.plugins: Plugin framework for installing and managing third-party plugins.
The following features cannot be enabled by themselves and are only enabled when using the full preset:
file_transfer: File transfer extension to MQTT.exhook: External gRPC hooks.opentelemetry: OpenTelemetry exporter.
-
#17768 Added support for sourcing
node.cookiefrom a file using thefile://URL form.Operators can now set
node.cookie = "file:///path/to/cookie"(or point theEMQX_NODE__COOKIEenvironment variable at afile://URL) so the cluster secret is not stored as plain text in the configuration. The referenced path may be a regular file or a FIFO (named pipe); it is read once when the node boots. When a FIFO is used, the orchestrator must write the cookie to it on each boot, before any otheremqxcommand is invoked (such asemqx ctl), because later commands obtain the cookie from the already-running node rather than re-reading the file.The resolved cookie is now passed to the Erlang VM directly and is no longer written to the generated
data/configs/vm.*.argsfile, so the secret is not persisted to disk during boot. -
#17803 When EMQX is booted with
EMQX_FEATURES=ESSENTIAL, the Erlang code loading mode now defaults tointeractiveso that the.beamfiles of disabled features are loaded on demand instead of all at boot. This significantly reduces the resident memory footprint of an essential-mode node, since the modules belonging to skipped features never become resident. The mode can still be overridden by settingCODE_LOADING_MODEexplicitly. -
#18451 Added support for reading boot-time environment variables from
etc/emqx.env(/etc/emqx/emqx.envon RPM and DEB installations,/opt/emqx/etc/emqx.envin the Docker image).The file lists
EMQX_FEATURESandEMQX_SECURITY_PROFILEwith their defaults commented out and a description of what each one does. These variables are read beforeemqx.confis parsed, so they cannot be set inemqx.conf. Theemqxcommand sources the file on every invocation, so a service start, a foreground start, andemqx ctlall see the same values. Values in the file override the inherited environment. Package upgrades keep edits to the file. -
#18452 Added
security_profileandfeature_presetto the node information returned byGET /nodesandGET /nodes/{node}.security_profileislegacyorhardened.feature_presetisfull,essential, orcustom. Both values are fixed when the node boots, so the list view shows when nodes in a cluster run with different settings. Stopped nodes do not report these fields. -
#18453 Added the
security_profile_divergencealarm.Nodes running the
hardenedsecurity profile (EMQX_SECURITY_PROFILE) periodically check the security profile of the other running nodes in the cluster, and raise the alarm when another running node runs thelegacyprofile. Nodes running thelegacyprofile do not run the check, and nodes running an older EMQX release without security profiles count aslegacy. The alarm message names thelegacynodes, and the alarm details keep the current node list while the alarm is active. The alarm clears on its own once every running node runs thehardenedprofile, or once the lastlegacynode leaves the cluster.The alarm is expected for a short time during a rolling upgrade that changes the security profile. An alarm that stays active points to nodes that were not restarted with the new
EMQX_SECURITY_PROFILEvalue. -
#18471 The
node_dumpdiagnostic script now includes theEMQX_FEATURESandEMQX_SECURITY_PROFILEsettings from the boot-time environment fileetc/emqx.env, when the file exists. Other variables in the file are not collected. -
#18557 #18609 Added the
node.default_listener_addressconfiguration option. It sets the address of MQTT listeners, gateway listeners, and the Dashboard HTTP listener when theirbindhas no explicit address, such as a bare-port bind (bind = 1883). Valid values:loopback(bind 127.0.0.1),nodename(bind the address in the host part of the Erlang node name, resolving it first when it is not an IP address),all(bind 0.0.0.0), a literal IPv4/IPv6 address, or a hostname to resolve at boot. When the option is not set, the security profile decides the default address as before. An explicitIP:portbind always wins. The option can also be set with theEMQX_NODE__DEFAULT_LISTENER_ADDRESSenvironment variable.The official Docker image sets
EMQX_NODE__DEFAULT_LISTENER_ADDRESS=all, so defaulted listeners stay reachable through published container ports regardless of the security profile.Listener views now report
resolved_address: the IP address a listener is actually bound to on the node it runs on, alongside the existingbindfield.bindkeeps showing the configured value, including the port;resolved_addressshows the IPbindresolves to after the security profile ornode.default_listener_addressapplies, without the port, which can differ from the address inbindfor a bare-port bind such asbind = 1883.A second field,
resolved_address_from, reports whyresolved_addresshas its value:bindwhen the listener's ownbindalready sets an explicit address,0.0.0.0for all interfaces,127.0.0.1for loopback,nodename, or the literalnode.default_listener_addressvalue when it is a hostname or IP address.Both fields are node-local:
GET /api/v5/listeners/:idreports the values for the node handling the request, andGET /api/v5/listenersreports them per node undernode_status, since a listener with the same ID can resolve to different addresses on different nodes in the same cluster, for example whennode.default_listener_addressis set tonodename.emqx ctl listenersprints both fields alongside the existinglisten_on. -
#18628 Data backup exports now record the exporting node's security profile.
Restoring a backup that was exported under the
legacysecurity profile onto a node running thehardenedsecurity profile can carry over data and configuration that behaves differently once restored:- Bare-port MQTT, gateway, and Dashboard HTTP listener binds resolve to loopback instead of all interfaces.
- An authenticator chain left empty or disabled starts denying every client instead of allowing them.
- A restored Dashboard account still on the default password can no longer log in.
- Authentication and authorization backend failures that were previously ignored now deny the operation.
Importing such a backup now needs the
--allow-security-profile-mismatchCLI flag, or theallow_security_profile_mismatchAPI parameter, so an operator gets a chance to review these differences instead of discovering them after clients stop connecting or logins start failing. A backup that predates this change is treated the same as one exported underlegacy. Restoring into a node runninglegacyis never affected.
Plugins
-
#18455 Under the
hardenedsecurity profile,emqx ctl plugins allow <Name-Vsn>now requires thesha256:<hex>argument. The grant binds the plugin package to the given SHA-256 digest. Only an upload whose bytes match the digest is installed. A grant without a digest is refused with a message that shows the required command. A node running thehardenedprofile also refuses a grant without a digest that a cluster peer sends to it.The
legacysecurity profile is unchanged: thesha256:<hex>argument stays optional.
Packaging
- #17335 After installation from an RPM or DEB package, the directory
/opt/emqxis created and populated with convenience symlinks (bin,data,etc,lib,log,plugins,releases,erts-*) pointing at the scattered FHS paths used by the package. Operators can now use the same/opt/emqx/...paths as in the official Docker image, regardless of how EMQX was installed.
Performance
-
#17583 Improved JSON encoding and decoding performance.
As part of this change, floating-point numbers in JSON output are now formatted consistently with the Erlang/OTP standard, which may differ slightly from previous releases (for example, switching to scientific notation a little earlier).
-
#18033 In
ESSENTIALfeature mode (and any deployment with the dashboard/management client-info API disabled), EMQX no longer performs the periodic per-connection statistics reporting that only feeds theGET /clientsendpoint, reducing per-connection overhead at high connection counts. -
#18424 Plugin configuration schemas (
config_schema.avsc) are now read from the installed plugin package each time a plugin configuration is validated, instead of being kept in memory for every installed plugin.When the schema file of an installed plugin is missing or unreadable, plugin configuration validation now reports the file error.
-
#18688 The number of dirty I/O scheduler threads (
+SDio) can now be configured vianode.dirty_io_schedulers. The defaultautokeeps the previous fixed value of 8 on nodes wherenode.schedulersresolves to more than 2, and uses 4 on smaller nodes (for example, a 2 vCPU container or cgroup). This reduces boot-time memory footprint on small nodes while keeping enough threads to overlap blocking I/O operations.
Bug Fixes
Core MQTT Functionalities
-
#18010 Malformed MQTT packets sent by clients are no longer logged as broker errors.
Such invalid input is now counted in the connection shutdown counters of the listener, reducing alert noise from port scanners, protocol fuzzers and misbehaving clients. Parse errors that carry packet-specific detail share a single
frame_errorcounter, so malformed packets can no longer create new counter names.Details of the parse error, including the offending bytes, are reported through tracing: start a trace on the client ID, IP address or topic to inspect them.
-
#18027 Changed shared-subscription handling so EMQX disconnects clients that attempt a shared subscription while shared subscriptions are disabled.
When
mqtt.shared_subscriptionis set tofalseand a client sends a SUBSCRIBE containing a shared topic filter ($share/...or$queue/...), EMQX now closes the network connection, as required by the MQTT specification for protocol errors. MQTT 5.0 clients first receive a DISCONNECT packet with reason code0x9E(Shared Subscriptions not supported); MQTT 3.1/3.1.1 clients simply get the connection closed.Previously such a SUBSCRIBE was answered with a failure reason code in the SUBACK and the connection was kept open.
-
#18116 When
strict_modeis enabled (the default), an MQTT v5 packet that includes a non-repeatable property more than once (for example, twoSession-Expiry-Intervalproperties in a CONNECT packet) is now rejected as a protocol error, instead of silently using the last value.User-Property, which the MQTT specification allows to repeat, is not affected.Operators can restore the previous lenient behavior by setting
strict_mode = falsein the listener configuration. -
#18438 Fixed an off-by-one in the size check applied to outgoing packets.
A packet whose serialized size was exactly the client's
Maximum Packet Sizewas discarded and logged asframe_is_too_large, instead of being delivered. Only packets larger than the limit are now discarded, which is what MQTT 5.0 requires. Packets below the limit are unaffected. -
#18470 Fixed an issue where EMQX acknowledged but did not deliver a retransmitted QoS 2 PUBLISH packet when the original packet had not been received before the publisher disconnected.
This fix partially reverts the QoS 2 duplicate-handling change introduced in #16721. After the awaiting-PUBREL state expires, a retransmitted QoS 2 PUBLISH packet is again treated as a new QoS 2 exchange and may be delivered to subscribers more than once. Starting with EMQX 7.0.0, awaiting-PUBREL state expiration will be disabled by default, preventing such redelivery.
-
#18487 Reduced log volume for PUBACK, PUBREC, PUBREL, and PUBCOMP packets that carry an unknown Packet Identifier. These events are logged at debug level and remain available through client tracing.
-
#18523 Changed shutdown ordering so EMQX stops MQTT listeners before stopping applications.
Previously, listeners kept accepting and processing client traffic while the applications behind the publish path were already stopped. Publishing clients could then trigger a burst of
hook_callback_exceptionerrors in the log, for example from the rule engine, until the listeners stopped a few seconds later. Listeners now stop first, so no client traffic is processed during application shutdown.The node now also reports itself as not running in
GET /statusas soon as shutdown begins, so load balancers stop routing new connections to it. -
#18585 Fixed session takeover so EMQX ends a session that does not outlive its connection when another connection takes over the same client ID, as the MQTT specification requires. This covers MQTT 5.0 clients connecting with Session Expiry Interval 0 and MQTT 3.1.1 clients connecting with Clean Session 1.
Before this fix, the new connection could inherit the old session's subscriptions and queued messages, and a will message with a Will Delay Interval greater than zero was silently dropped. Now the new connection starts a fresh session (CONNACK Session Present 0), the old connection receives DISCONNECT with reason code 0x8E (Session taken over), and its will message, if any, is published at the takeover.
Access Control
-
#18246 Added delayed-message authorization to the hardened security profile.
- In the hardened security profile, delayed messages are reauthorized when replayed. Delayed messages from MQTT and gateway clients carry a restricted authorization context. EMQX checks current publish authorization rules and ban records before replay. A message that was authorized when scheduled can be dropped when replayed.
- In the hardened security profile, pending delayed messages created before the upgrade are dropped when replayed because they do not contain an authorization context. The legacy profile continues to replay them.
- Fixed mountpoint handling in multiple gateways. Gateways consistently pass logical, unmounted topics to authorization. When
authorization.include_mountpoint = false, EMQX checks the logical topic. When it istrue, EMQX applies the mountpoint once for the authorization check. In both cases, EMQX applies the mountpoint once before publishing or subscribing. - The GBT 32960, JT/T 808, LwM2M, NATS, and STOMP gateways no longer pass pre-mounted topics to publish authorization. This prevents authorization from checking a double-mounted topic when
authorization.include_mountpoint = true. - GBT 32960
dnstream, JT/T 808proto.dn_topic, and LwM2M command auto-subscriptions no longer apply the mountpoint before authorization. - JT/T 808
proto.up_topicandproto.dn_topicare now relative to the gateway mountpoint. Their defaults changed fromjt808/${clientid}/${phone}/upandjt808/${clientid}/${phone}/dnto${phone}/upand${phone}/dn. - MQTT-SN idle QoS -1 publishes and will messages now apply the configured mountpoint; these paths previously published without it.
- NATS publish authorization checks the MQTT topic converted from the NATS subject before applying the mountpoint. NATS JWT permissions and EMQX authorization no longer check a pre-mounted topic.
- Fixed duplicate processing of delayed messages by other systems, such as bridges, the retainer, and the schema validator. EMQX processes delayed messages only during actual replay. Direct internal publishers (for example, plugins) must invoke the
message.ingresshook to schedule delayed messages. Directemqx:publish/1calls and management API publishes to$delayed/...bypass message ingress and fail to schedule delayed messages.
-
#18458 Upgraded
oidccto3.2.3.The upgrade fixed Dashboard SSO (OIDC) login timeouts (
INTERNAL_ERROR: exit,{timeout,{gen_server,call,[...]}}) while the provider configuration worker was busy refreshing its cached configuration. -
#18576 The OIDC SSO configuration API (
GET /api/v5/sso/oidc) now returnsclient_jwksasnonewhen no client JWKS is configured, matching the CLI output. Previously the value was masked as******even when nothing was configured. A configured client JWKS remains masked. -
#18580 Redacted sensitive configuration values in the
conf.hoconfile produced by thebin/node_dumpscript.Values marked as sensitive in the configuration schema, such as
dashboard.default_passwordandlicense.key, are now written as******. Before this fix, the script redacted only a fixed list of key names, so these values were written in plain text.
Multi-tenancy
-
#18423 Data Backup imports performed by a namespaced administrator now apply only that namespace's configuration. Cluster-wide settings found in the namespaced configuration, such as authentication, authorization, ExHook, or listeners, are skipped with a warning instead of being written to the global configuration.
-
#18466 Fixed listing of backup files for namespaces whose names contain special characters.
Previously, the backup file list was empty for a namespace whose name contained characters such as
*,?,{,},[or], even though the backup files existed on disk. The listing now treats the namespace name as a literal directory name. -
#18539 Fixed the multi-tenancy client list not following a persistent session that reconnects under a different namespace.
Previously, when a client resumed an existing session (
clean_start=false) after its namespace changed,GET /api/v5/mt/ns/{ns}/client_listkept listing the client under the old namespace, and the new namespace's list did not include it. The client list and the per-namespace client count now always reflect the namespace the client connected with. This also fixes the client disappearing from the list after resuming a durable session.
Data Integration
-
#18300 Blank certificate file fields in a connector's TLS settings are now treated as not configured regardless of the
verifymode. Previously, blank client certificate fields were rejected with a validation error whenverifywas set toverify_peer, although client certificates are optional for peer verification. -
#18392 Fixed an issue where aggregated Actions (S3, S3Tables, Azure Blob Storage, Snowflake Aggregated) with the same name but in different namespaces would share the same working directory for their temporary files.
-
#18449 Fixed a rare race condition in which the PostgreSQL Action encountered a
sock_closederror while writing data and incorrectly treated it as unrecoverable. EMQX treats the error as recoverable. -
#18767 Fixed the RocketMQ connector being reported as belonging to a namespace it does not belong to.
The RocketMQ connector has its own
namespaceconfiguration field, holding the RocketMQ instance namespace. Connector API responses returned that value under the same JSON key used for the EMQX namespace, so the Dashboard treated the connector as owned by a namespace of that name. It showed "Only the administrator of namespace can perform operations on the connector", and opening the connector failed with "Managed namespace not found".The
namespacefield in connector API responses now always holds the EMQX namespace. The RocketMQ instance namespace is no longer returned, and it is kept unchanged when a connector is updated without it.
Rule Engine
- #18527 Fixed repeated
badargerrors in the log when a message was published while the schema validation, message transformation, or rule engine topic index table was unavailable. Such a publish now proceeds as if no validation, transformation, or rule matched the topic, and the broker logs a throttledtopic_index_table_missingmessage instead of one error per publish. The index tables now also survive a restart of their owner process, and the hooks are removed before the tables during application shutdown, which removes the known windows where a publish could find a table missing.
Clustering
-
#18409 Fixed Cluster Linking for a link whose
serverfield lists more than one address.Such a link now connects through all listed addresses: each connection prefers one of the addresses in turn and fails over to the others when it cannot connect. Before this change the link could not connect, and no link could be created, updated or deleted until the node was restarted.
-
#18447 Fixed an issue where changes to
base.hoconcould be ignored after a node synchronized configuration from another cluster member. Configuration synchronization no longer persists the peer'sbase.hoconvalues intocluster.hocon, so localbase.hoconchanges take effect after restart unless explicitly overridden by cluster configuration. -
#18537 Fixed Cluster Linking to classify temporary message-forwarding connection errors as recoverable. Messages affected by transient network outages are now buffered and retried instead of being counted as failed.
Gateway
-
#18312 Fixed an issue in plaintext CoAP UDP listeners with connection mode enabled where a rejected request from another source could redirect subsequent downlink messages.
-
#18436 Fixed an issue where NATS Gateway internal JWT authentication did not enforce account JWT
exp/nbfclaims or account-level user revocations.Expired or not-yet-valid account JWTs and user JWTs revoked by the account are now rejected during authentication. Existing connections are disconnected when the earlier of the user JWT or account JWT expiration is reached. Malformed resolver-preloaded account JWTs are rejected during gateway configuration validation.
-
#18494 Fixed CoAP gateway clients reporting the internal keepalive check interval instead of the configured heartbeat interval.
The client information returned by the gateway API and
emqx ctl gateway-clients list coapnow reports the configured heartbeat value in seconds. -
#18504 Fixed STOMP frame parsing of escaped header characters and CRLF line endings.
The STOMP gateway now decodes the escape sequences
\c,\r,\n, and\\in header names and values, as required by STOMP 1.2. CONNECT and CONNECTED frames are exempt: STOMP 1.2 excludes them from header escaping for backward compatibility with STOMP 1.0, so their headers, including a password containing a colon or a backslash, pass through unchanged. In other frames, an undefined escape sequence is now rejected as a frame error.The gateway now also accepts CRLF (
\r\n) line endings in frames and CRLF heartbeats. Before this fix, clients using CRLF line endings could not connect. -
#18700 Fixed an issue where saving an existing NATS Gateway configuration from the Dashboard could replace its authentication credentials with the masked value displayed in the form, causing NATS clients to fail authentication.
Authentication credentials are now preserved when users update other Gateway settings without changing the authentication configuration.
NATS Gateway authentication settings now reject duplicate authentication methods and credential entries, including duplicate NKeys and JWT account entries, to prevent ambiguous authentication behavior.
Plugins
-
#18188 Hardened the plugin framework package and runtime integrity checks.
- Operators must run
emqx ctl plugins allowbeforeemqx ctl plugins install, enforcing the same allow gate as the HTTP upload API. - Plugin API callback responses are restricted to an allow-list of safe response headers; browser-sensitive headers (such as
set-cookie,location,access-control-*,content-security-policy, and other authentication or security policy headers) and custom headers without thex-plugin-prefix are stripped. - Added the
plugins.package_limitsconfiguration to bound plugin package extraction:max_package_size(default10MB),max_decompressed_size(default50MB),max_file_count(default10000),max_path_depth(default32), andmax_extraction_time_ms(default60s, also used as the RPC timeout for cluster package copying). Packages violating these limits are rejected before or during extraction. Tar entries escaping the install directory (path traversal) are also rejected.
- Operators must run
-
#18468 The hot-upgrade (relup) plugin now validates the target version string and checks upgrade-path compatibility before it modifies any files. An incompatible or malformed upgrade package is rejected without deleting or overwriting the installed release.
-
#18540 Shipped the default configuration file (
priv/config.hocon) in theemqx_relupplugin package. Installing the plugin no longer logs a repeatedfailed_to_copy_plugin_default_hocon_configwarning.
ExHook
-
#18464 Fixed a rare crash in the ExHook manager when an ExHook server became unhealthy during a configuration update. The manager now keeps the configured server order and continues serving configuration changes while the server reconnects.
-
#18473 Fixed ExHook authentication and authorization behavior when no callback server is running. The legacy security profile now honors the configured
failed_action, while the hardened security profile remains fail-closed.
Observability
-
#17602 Added configuration-backed
emqx ctl log outputscommands so CLI changes to logger outputs stay consistent with logger configuration managed by the HTTP API and Dashboard. -
#17912 Fixed a security vulnerability in OpenTelemetry W3C Baggage header extraction (GHSA-64w2-whjg-q7q7). Previously, the inbound
baggageheader was decoded with no byte or entry-count limit, and a malformed key-value pair would crash the process. Extraction is now capped at 8192 bytes and 180 entries as recommended by the W3C Baggage specification, and malformed pairs are skipped instead of crashing. -
#18521 Added an identifying
labelto the connection shutdown report emitted when a connection exceeds a force-shutdown limit (force_shutdown.max_mailbox_sizeorforce_shutdown.max_heap_size).For an established connection, the
labelholds the client ID. For a connection shut down before CONNECT completes, it holds the listener name and peer address. Previously, the report contained only the limit and the measured value, so the operator could not identify the affected connection. -
#18696 Fixed an issue where querying the audit log could return an error for records created by SSO-authenticated users.
Management
-
#18289 Fixed an issue where JSON-compatible Unicode escape sequences in quoted HOCON strings and keys were left escaped instead of being decoded.
-
#18403 Fixed
emqx ctlcommands printing non-ASCII characters as\x{...}escapes or invalid bytes when the command runs in a shell without a UTF-8 locale (for example over non-interactive SSH, cron, orLANGunset).The Erlang VMs started by the
emqxscript (the node itself,emqx ctl,emqx eval,emqx remote_console, andemqx escript) now always read and write standard I/O as UTF-8. -
#18444 Fixed the byte-size units
bandBrequiring quotes in configuration files.max_packet_size = 1MBwas accepted, butmax_packet_size = 1Bfailed to parse and had to be written as"1B". All byte-size units are now accepted without quotes. -
#18509 Fixed message paging in
GET /clients/{clientid}/mqueue_messagesandGET /clients/{clientid}/inflight_messages.These APIs limit the total payload size of one response page by the
max_payload_bytesparameter (default 1MB). When this limit cut a page short, the returnedmeta.positionpointed past the messages that were left out, so requesting the next page from that position skipped them. This could look like lost messages, for example amqueue_lencount higher than the number of messages the API returns. Nowmeta.positionpoints at the last returned message, and the next page continues with the first message that was left out. -
#18544 Fixed
GET /clients_v2returning a cursor after returning all clients with memory sessions. Following that cursor returned an empty page. The API no longer returns a cursor when no more data is available. -
#18558 Fixed
GET /clients_v2ignoring thefieldsquery parameter. -
#18590 Fixed the output of
emqx stopwhen the node is not running.The command reported
Node <name> not responding to pings.twice and then failed withGraceful shutdown failed PID=[]. It now reports the unreachable node once and does not print a shutdown failure for a node it could not find. The exit code is unchanged. -
#18619 Fixed
GET /nodes/{node}returning a 500 Internal Server Error instead of a 400 Bad Request when the target node becomes unreachable between the API's liveness check and the RPC that fetches its info, for example when the node concurrently leaves the cluster.
Deployment and Security
-
#17921 Upgraded the
protobufdependency to v0.17.0. This dependency is used only for SBOM generation and is not part of the EMQX runtime. The upgrade picks up a fix for an unbounded-recursion denial of service when decoding deeply-nested messages (GHSA-rv48-qqj5-crxg) and Elixir 1.19/1.20 compiler warning fixes, and replaces the previously pinned development ref with the official release. -
#18706 Avoid logging sensitive information in debug mode.
Running
bin/emqxcommands withDEBUG=1orDEBUG=2no longer prints the Erlang cookie or the license key in the shell trace output.