Files
Mattia Eleuteri bb3ddee233 fix(rabbitmq): bound the topology operator Secret cache
messaging-topology-operator 1.14.2 caches every Secret in the cluster: its
ClusterRole grants secrets list/watch cluster-wide with no namespace or label
restriction, so the controller-runtime cache holds the whole set. Against the
vendored 500Mi limit this OOMKills on any cluster that has grown enough
Secrets, and it never recovers, because the initial LIST alone no longer fits.
While it is down no topology CR reconciles, for any tenant.

Observed on a cluster with 8211 Secrets across 291 namespaces (293 MB, 87% of
it Helm release history): a flat 333Mi working set for days, then the ceiling,
then a permanent crashloop. The operator was caching 8211 Secrets to read 2.

Upstream fixed this by gating the Secret informer on a label, released in
v1.20.0, and by bypassing the cache for Service reads, released in v1.20.1.
Refresh the vendored manifest to v1.20.2, the current patch release carrying
both, through the package's existing make update pipeline.

The bump alone would break every install: the app chart's credentials Secret is
referenced by User.spec.importCredentialsSecret, and from 1.20 the admission
webhook rejects a user-provided Secret that does not carry
rabbitmq.com/topology-operator=true. Label it in the same change, pin the
label in the chart's unit tests, and give the e2e RabbitMQ a user so the
webhook and the User reconcile are exercised against the real operator.

The RabbitmqCluster default-user Secret needs no label, it is read uncached via
apiReader, and the Status.Binding contract is unchanged since 1.14.2, so the
vendored cluster-operator 2.9.0 stays compatible and does not move here.

Refs #4128

Signed-off-by: Mattia Eleuteri <mattia@hidora.io>
Assisted-by: LLM
2026-09-29 17:08:17 +02:00
..
2024-08-12 14:47:11 +02:00
2024-07-16 17:31:52 +02:00

Managed RabbitMQ Service

RabbitMQ is a robust message broker that plays a crucial role in modern distributed systems. Our Managed RabbitMQ Service simplifies the deployment and management of RabbitMQ clusters, ensuring reliability and scalability for your messaging needs.

Deployment Details

The service utilizes official RabbitMQ operator. This ensures the reliability and seamless operation of your RabbitMQ instances.

storageClass is annotated as immutable in the chart schema — see docs/storage-immutability.md for the contract and which consumers enforce it.

Backups

RabbitMQ is bound out-of-the-box by the platform cozy-default BackupClass, so a Plan or an ad-hoc BackupJob referencing cozy-default backs the instance up with no per-release wiring — see Backup Classes.

The scope is definitions only — vhosts, users, permissions, queues, exchanges, bindings, policies and parameters, exported through the management API. Message payloads are not included, and there is no point-in-time recovery; back up the messages on disk with a Velero volume snapshot instead if you need them. Restore is a merge, not a reset: importing definitions creates or updates what the backup contains and never deletes, so anything created after the backup survives a restore, and a to-copy restore into a separate instance imports the source's users (with their password hashes) into the target.

Parameters

Common parameters

Name Description Type Value
replicas Number of RabbitMQ replicas. int 3
resources Explicit CPU and memory configuration for each RabbitMQ replica. Every resource left unset here is taken from resourcesPreset. object {}
resources.cpu CPU available to each replica. quantity ""
resources.memory Memory (RAM) available to each replica. quantity ""
resourcesPreset Default sizing preset. It supplies every resource resources does not set, not only a resources left empty entirely. string s1.nano
size Persistent Volume Claim size available for application data. quantity 10Gi
storageClass StorageClass used to store the data. string ""
external Enable external access from outside the cluster. bool false
tls TLS configuration. TLS is off unless tls.enabled is set to true. object {}
tls.enabled Enable TLS for AMQPS (5671) and Management HTTPS (15671). Applies whether or not external is set. Plaintext AMQP (5672) and management (15672) keep serving unless disableNonTLSListeners is set as well. Moves the port key of the <release>-default-user Secret from 5672 to 5671, and the metrics port from 15692 to 15691. Defaults to false. bool false
tls.disableNonTLSListeners Close the plaintext AMQP (5672) and management (15672) listeners. This is a broker-level switch: it drops in-cluster clients as well as external ones, and it cannot be combined with users or vhosts. Erlang distribution (25672) and epmd (4369) stay plaintext regardless. Defaults to false. bool false
version RabbitMQ major.minor version to deploy string v4.2

Application-specific parameters

Name Description Type Value
users Users configuration map. map[string]object {}
users[name].password Password for the user. string ""
vhosts Virtual hosts configuration map. map[string]object {}
vhosts[name].roles Virtual host roles list. object {}
vhosts[name].roles.admin List of admin users. []string []
vhosts[name].roles.readonly List of readonly users. []string []

TLS scope

This chart can add TLS listeners for AMQP (port 5671) and the management HTTPS interface (port 15671) using cert-manager. Enabling it adds encrypted endpoints without closing the unencrypted ones — but it does change which endpoint clients are pointed at, and tls.disableNonTLSListeners can close the plaintext ones outright. Both are covered below.

TLS is off unless you ask for it. tls.enabled defaults to false and nothing else turns it on — in particular external does not. Publishing a broker outside the cluster and encrypting it are separate decisions, so an existing instance keeps its listeners and its published connection details across an upgrade of this chart, and every consequence listed below follows an explicit edit rather than arriving on its own.

Enabling TLS does not close plaintext, inside or outside the cluster. AMQP on 5672 and management on 15672 keep listening whenever TLS is on, and when external is true they are published on the public LoadBalancer alongside the TLS ports. Enabling TLS adds encrypted endpoints; using them is the client's choice rather than something the endpoint enforces. Treat an external: true RabbitMQ as reachable in plaintext from outside the cluster — with TLS on or off — and restrict it at the network layer if that is not acceptable.

Plaintext cannot be closed on the public side alone. There is no operator-supported way to drop a port from the published Service by itself: spec.override.service.spec.ports is applied as a strategic merge patch keyed on port, so it can add or mutate entries but never remove them, and the operator regenerates its default port set on every reconcile. The only switch this chart exposes is tls.disableNonTLSListeners, described below, and it closes plaintext everywhere at once rather than only on the public side — in-cluster clients included.

A narrower variant, closing AMQP at the broker while leaving the operator's own management path on plaintext so that chart-managed users and vhosts keep working, looks reachable through spec.rabbitmq.additionalConfig. It is not. The operator pins the readiness probe to the amqp port unless spec.tls.disableNonTLSListeners is set, so writing listeners.tcp = none through additionalConfig alone leaves every pod failing readiness for good. The flag is the only supported way to close the plaintext listeners, and it closes all of them at once.

Setting tls.enabled: true on a running instance changes three things. Plan for them before flipping it.

The broker pods roll once, because the operator mounts the new TLS Secret into the pod template and that is a StatefulSet template change. The instance gains a dependency on cert-manager, since the operator will not finish reconciling until the Secret has been issued.

And the published metrics port changes. The operator exposes either prometheus (15692) or prometheus-tls (15691) on its Service, never both — deliberately, so that a scrape config selecting every port does not count each node twice — and that choice keys off TLS alone. Enabling TLS therefore withdraws 15692 from the Service and publishes 15691 in its place. The broker itself keeps listening on 15692, but a ServiceMonitor or VMServiceScrape selecting the port by the name prometheus stops matching. Retarget it to prometheus-tls, which is HTTPS under the release CA and so needs a tlsConfig carrying the trust anchor described below. The chart does not paper over this by re-adding 15692 through spec.override.service: the operator drops any port name it does not own on each reconcile and the override would reinstate it without a NodePort, which on a LoadBalancer means a fresh allocation and another reconcile every round.

And the connection details handed to clients change. <release>-default-user is the Service Binding object this chart grants to the tenant, and the operator rewrites its port key from 5672 to 5671 as soon as spec.tls.secretName is set — gated on TLS alone, not on whether the plaintext listener is still open. A client that hardcodes :5672 keeps working, because that listener does stay open. A client that resolves its endpoint from the binding — the documented way to consume it — follows the change to 5671 and must speak TLS from then on, and it cannot verify the certificate until the trust anchor below is obtainable. Per-plugin port keys (mqtt-port, stomp-port, stream-port and the web variants) move to their TLS values the same way. This is the main reason tls.enabled is opt-in rather than something external implies: it moves clients that read the binding correctly, and it moves them the instant the flag is set, before any of them has had a chance to pick up the trust anchor.

The plaintext management listener is load-bearing. messaging-topology-operator is what reconciles the User, Vhost and Permission objects this chart creates from users and vhosts, and it talks to the broker over the management API. Once TLS is on it would switch to HTTPS on 15671 and verify the broker against the operator process's system certificate pool, which cannot contain a per-release self-signed CA — so every user and permission would fail to reconcile with certificate signed by unknown authority. The chart therefore annotates the cluster with rabbitmq.com/operator-connection-uri pointing at plaintext http://…:15672, keeping that operator on the in-cluster plaintext port. This affects only the operator's own management connection, never broker traffic, and it is why tls.disableNonTLSListeners cannot be combined with users or vhosts.

Erlang distribution and epmd stay plaintext no matter what you set. Inter-node distribution (25672) and epmd (4369) are carried on the headless Service, which disableNonTLSListeners never touches — the flag does not apply to them. So tls.disableNonTLSListeners: true does not mean "TLS everywhere": it means every client-facing listener is TLS-only while inter-node traffic stays in the clear. No TLS field on the cluster reaches them: spec.tls.caSecretName, the only one left, turns on client-certificate verification for the client-facing listeners and says nothing about distribution.

Closing the plaintext listeners

tls.disableNonTLSListeners is opt-in and defaults to false, so an upgrade never closes a listener a client is already using. Setting it to true writes listeners.tcp = none into rabbitmq.conf, a broker-level directive: the socket is not opened on any interface, so in-cluster clients lose plaintext at the same instant external ones do. There is no setting that closes it externally while keeping it internally. Move every client to 5671 and 15671 first, then set the flag.

Two consequences to know before enabling it:

  • It puts the whole cluster out of reach of messaging-topology-operator, and the chart refuses to render if users or vhosts are set. That operator reconciles those objects over the management API, and the flag forces its connection onto TLS, computed as TLSEnabled() && (!connectUsingHTTP || DisableNonTLSListeners()). The rabbitmq.com/operator-connection-uri annotation is the one setting that short-circuits that calculation before it is reached, and it is no escape here, because the port it names is the port the flag closes — which is why the chart withdraws it as the flag goes on. The operator then verifies the broker against its own process's system trust pool, which cannot contain a per-release CA, so every object it owns stops reconciling. Note the scope twice over. That calculation reads the RabbitmqCluster object, not the origin of the topology CR, so moving users and vhosts out of values.yaml into hand-written objects does not work around it — they fail the same way, just without the chart's refusal to warn you. And it is not only the three kinds this chart creates: Binding, Exchange, Federation, OperatorPolicy, Permission, Policy, Queue, SchemaReplication, Shovel, TopicPermission, User and Vhost all run on the one reconciler and share the one deletion path. SuperStream is the exception, having its own reconciler and no finalizer, but the Queue, Exchange and Binding objects it owns are on the list. Once this flag is on, manage users and permissions through rabbitmqctl or a definitions import instead. The same calculation governs deletion, so a topology object that is left behind does not merely stop reconciling: its deletion.finalizers.<plural>.rabbitmq.com finalizer can no longer be removed either, and the object sits in Terminating until the flag comes back off or the whole RabbitmqCluster goes away.
  • The plaintext metrics port closes with it, leaving only prometheus-tls on 15691.

Removing the topology objects is its own upgrade

Clearing users and vhosts and setting tls.disableNonTLSListeners: true in a single edit does not work, and the chart refuses to render it. Helm applies every object in the target release before it prunes the ones the release dropped, so the broker would close its plaintext listeners while the topology objects were still waiting to be deleted. Their operator needs the management API to release a finalizer, that API is now reachable only over a certificate it cannot verify, and the delete never completes — leaving the objects in Terminating, their users still live in the broker, and kubectl delete namespace on the tenant hanging, all while the upgrade reports success.

Do it as two upgrades instead:

  1. Remove users and vhosts from the values, along with any hand-written topology object in the namespace, and let that upgrade finish.
  2. Confirm the namespace is clear: kubectl get bindings.rabbitmq.com,exchanges.rabbitmq.com,federations.rabbitmq.com,operatorpolicies.rabbitmq.com,permissions.rabbitmq.com,policies.rabbitmq.com,queues.rabbitmq.com,schemareplications.rabbitmq.com,shovels.rabbitmq.com,topicpermissions.rabbitmq.com,users.rabbitmq.com,vhosts.rabbitmq.com --namespace <tenant-namespace> must return nothing bound to this release. Those are the twelve kinds messaging-topology-operator drives through one reconciler, and every one of them carries the same finalizer, so checking only the three the chart creates leaves the other nine to wedge. A tenant namespace can hold several RabbitMQ applications, so read spec.rabbitmqClusterReference.name on whatever comes back: an object naming a sibling release is reached through that broker, whose listeners this flag does not touch, and the chart's own check ignores it too. Objects in Terminating still count — a finalizer that has not come off yet means the operator has not finished with the broker.
  3. Set tls.disableNonTLSListeners: true.

A release already wedged this way recovers without data loss: unset tls.disableNonTLSListeners, wait for the finalizers to clear once the operator can reach the broker again, and then start over from step 1.

Verifying the server certificate

The certificates are issued by a per-release self-signed CA, so a client must be given that CA to verify the endpoint. The trust anchor is published as <release>.tenant-ca: an object holding ca.crt and nothing else, delivered through the core.cozystack.io/tenantsecrets API that the base tenant roles already grant.

Throughout this section <release> means the Helm release name, which is the application name prefixed with rabbitmq-. A RabbitMQ named orders is release rabbitmq-orders, so its trust anchor is rabbitmq-orders.tenant-ca and its certificate Secrets are rabbitmq-orders-tls and rabbitmq-orders-ca.

kubectl get tenantsecret rabbitmq-<name>.tenant-ca --namespace <tenant-namespace> \
  -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt

For an external endpoint, note what the certificate does and does not cover. When the platform supplies a root host the certificate carries rabbitmq-<name>.<host> as a SAN, but this chart creates no DNS record for that name — external: true renders a bare LoadBalancer Service, with no Ingress and no external-dns annotation. Verifying by hostname therefore requires pointing that name at the LoadBalancer address yourself. A client connecting straight to the IP will fail hostname verification against the SAN list, since it contains in-cluster names and the unresolved external name only.

<release>.tenant-ca is the only object that hands over the CA certificate without also handing over a private key, which is why it exists. The cert-manager Secrets are deliberately not granted to tenants: <release>-ca stores the CA private key, so read access would let the holder issue certificates for anything, and the leaf <release>-tls stores the server private key next to the same ca.crt. RBAC has no key-level filter, so granting either to deliver a trust anchor would deliver a key with it.

Verification runs one way only. The server presents a certificate that a client can verify with the CA above; the server does not ask for one in return. The operator's switch for that is spec.tls.caSecretName, which this chart leaves unset. Setting it writes ssl_options.cacertfile and ssl_options.verify = verify_peer for AMQP, plus a cacertfile for the management and metrics listeners. Note what that does not do: verify_peer on its own validates a client certificate when one is offered and still admits a client that offers none, and the operator writes neither fail_if_no_peer_cert nor management.ssl.verify, so making certificates mandatory needs broker configuration that neither the operator nor this chart supplies. Clients continue to authenticate by username and password — carried inside the TLS session rather than in the clear, but still a password rather than a certificate.

Turning TLS off again

Disabling TLS on a release that had it needs one manual step. Helm removes the TenantProjection sentinel along with the Certificate objects, and Kubernetes garbage-collects rabbitmq-<name>.tenant-ca with it, so the trust anchor stops being projected as soon as TLS goes off — no stale anchor is left for an endpoint that has gone back to plaintext. What Helm cannot clean up is the raw CA material: cert-manager does not delete the Secrets it produced (enableCertificateOwnerRef is off), so rabbitmq-<name>-ca and rabbitmq-<name>-tls stick around, unused, holding private keys nobody references any more. Delete them by hand after disabling TLS.

Deleting the application leaves the same two Secrets behind. The chart renders the Certificate and Issuer objects and never the Secrets, and cert-manager does not own what it issued, so a Helm uninstall removes neither rabbitmq-<name>-ca, which holds the CA private key, nor rabbitmq-<name>-tls. After removing a RabbitMQ that had TLS on, delete both from the tenant namespace by hand:

kubectl delete secret rabbitmq-<name>-ca rabbitmq-<name>-tls --namespace <tenant-namespace>

Warning: the CA is issued with a 5-year duration and cert-manager renews the certificate as it approaches expiry, so a bundle copied once will eventually stop verifying. Re-read <release>.tenant-ca on a schedule, or mount it and let the kubelet refresh it, instead of baking ca.crt into a client image or a truststore built at release time.

Parameter examples and reference

resources and resourcesPreset

resources sets explicit CPU and memory configurations for each replica. Every resource it leaves unset is taken from the preset defined in resourcesPreset.

resources:
  cpu: 4000m
  memory: 4Gi

resourcesPreset sets named CPU and memory configurations for each replica. This setting is ignored if the corresponding resources value is set.

Presets follow a cloud-style <series>.<size> naming convention. Five series cover the full CPU-to-memory ratio range (t1 1:0.5, c1 1:1, s1 1:2, u1 1:4, m1 1:8) and each series ships eight sizes (nano through 4xlarge). The legacy flat names (nano, micro, small, medium, large, xlarge, 2xlarge) remain accepted as deprecated aliases and keep their original sizes, which do not follow one series: nano through small equal the t1 sizes of the same name, while medium equals c1.small rather than c1.medium.

See docs/operations/resource-presets.md for the full size matrix and the legacy-to-instance-type mapping.