KubeMQ
ConnectorsAMQP 1.0How-to guides

Authentication

How an AMQP 1.0 client authenticates to KubeMQ — SASL PLAIN with a KubeMQ JWT, SASL EXTERNAL with mTLS, ANONYMOUS for dev, and Casbin authorization.

This guide explains how a native AMQP 1.0 client proves who it is to the KubeMQ AMQP 1.0 connector (authentication / SASL) and what it may do once attached (authorization). It covers the three SASL mechanisms, how the client identity (ClientID) is derived, why container-id matters, and the audit events the connector emits.

On a stock dev broker, authentication is off and clients connect ANONYMOUS — so the examples clone-and-run with no credentials. SASL PLAIN with a KubeMQ JWT is the one credentialed mechanism that also runs on a stock broker; SASL EXTERNAL requires mTLS (see TLS and mTLS).

The three SASL mechanisms

The connector computes the SASL mechanism list per connection from the auth and TLS context and offers it in a fixed order: EXTERNAL → PLAIN → ANONYMOUS.

MechanismOffered whenCredentialIdentity (ClientID) becomes
PLAINalwaysRFC 4616 authzid\x00authcid\x00passwd; the password is the KubeMQ JWTauth on → ClientID from the JWT; auth off → sanitized SASL username, else the container-id
ANONYMOUSonly when the auth service is disablednonesanitized container-id
EXTERNALonly on an mTLS connection with a verified client certificatethe certificate itself (no JWT)the cert Subject CN, sanitized

PLAIN — the documented contract

PLAIN is the mechanism to use when authentication is enabled. The credential layout is the standard RFC 4616 triple authzid \x00 authcid \x00 passwd:

  • passwd (password) = the KubeMQ JWT. It is validated server-side; on success the connector takes the ClientID from the JWT's claims.
  • authcid (username) is audit-only / informational when auth is on. It does not become the identity — the JWT does. (When auth is off, the username is used as a convenience identity; see precedence below.)
  • The SASL initial-response binary is capped at 16 KiB. A larger response is rejected as a malformed/hostile peer. A KubeMQ JWT fits comfortably.
  • No SCRAM-SHA-256, GSSAPI, or Azure CBS. PLAIN is the only credentialed mechanism.

Most clients accept a (username, password) pair — put the JWT in the password slot:

// Azure/go-amqp — SASL PLAIN: username is audit-only, password is the KubeMQ JWT.
conn, err := amqp.Dial(ctx, "amqp://broker:5672", &amqp.ConnOptions{
    SASLType: amqp.SASLTypePlain("audit-username", os.Getenv("KUBEMQ_AMQP_JWT")),
})
if err != nil {
    log.Fatalf("dial (bad/expired JWT? auth-disabled broker?): %v", err)
}
defer conn.Close()
# python-qpid-proton — SASL PLAIN: username audit-only, password is the KubeMQ JWT.
# allow_insecure_mechs permits PLAIN over plaintext amqp://; use amqps:// in production.
conn = BlockingConnection(
    "amqp://broker:5672",
    user="audit-username",
    password=os.environ["KUBEMQ_AMQP_JWT"],
    allowed_mechs="PLAIN",
    allow_insecure_mechs=True,
)
// qpid-jms — the username/password passed to createConnection become the SASL
// PLAIN authcid/password (password = the KubeMQ JWT). Pin PLAIN on the URI.
String url = "amqp://broker:5672?amqp.saslMechanisms=PLAIN";
JmsConnectionFactory factory = new JmsConnectionFactory(url);
Connection connection = factory.createConnection(
        "audit-username", System.getenv("KUBEMQ_AMQP_JWT"));
connection.start();
// AMQPNetLite — an Address carrying User + Password makes the client negotiate PLAIN
// (password = the KubeMQ JWT). The host/port/user/password ctor avoids URL-encoding it.
var baseAddress = new Address("amqp://broker:5672");
var connectAddress = new Address(
    baseAddress.Host, baseAddress.Port,
    "audit-username", Environment.GetEnvironmentVariable("KUBEMQ_AMQP_JWT"),
    "/", baseAddress.Scheme);
var connection = await Connection.Factory.CreateAsync(connectAddress);
// rhea / rhea-promise — setting both username + password makes rhea select PLAIN
// (password = the KubeMQ JWT).
const connection = await container.connect({
  host: "broker",
  port: 5672,
  container_id: `kubemq-amqp10-js-${process.pid}`,
  username: "audit-username",
  password: process.env.KUBEMQ_AMQP_JWT,
});
// fe2o3-amqp — SASL PLAIN: username audit-only, password is the KubeMQ JWT.
let mut connection = Connection::builder()
    .container_id("amqp10-client")
    .sasl_profile(SaslProfile::Plain {
        username: "audit-username".to_string(),
        password: std::env::var("KUBEMQ_AMQP_JWT")?,
    })
    .open("amqp://broker:5672")
    .await?;

ANONYMOUS — the auth-off default

When the broker's authentication service is disabled, the connector offers ANONYMOUS. This is the mechanism the runnable examples rely on: no credential is sent and the identity falls back to the container-id. ANONYMOUS is only offered when auth is off — requesting it against an auth-enabled broker is rejected as an auth failure (it was never advertised).

EXTERNAL — mTLS, cert CN → ClientID

EXTERNAL is offered only when the TLS handshake presented a client certificate that the listener verified (the mTLS listener). The identity is the client certificate's Subject CN, sanitized to a valid ClientID; no JWT is needed. An empty CN is rejected. Because EXTERNAL depends on mTLS, it is covered alongside TLS — see TLS and mTLS.

Identity precedence

The connector resolves the client identity in this order, depending on the negotiated mechanism and whether auth is enabled:

  1. PLAIN + auth on → the JWT's ClientID claim.
  2. PLAIN + auth off → the sanitized SASL username (authcid); if empty, the container-id.
  3. EXTERNAL → the client certificate's Subject CN (sanitized).
  4. ANONYMOUS → the sanitized container-id.
  5. bare (no SASL) → the sanitized container-id; if empty, a generated amqp10-<uuid8>.

Sanitization caps the value at 256 characters and maps every character outside [a-zA-Z0-9_-] to _.

container-id is required — and must be stable for durable subscribers

Every AMQP 1.0 OPEN must carry a non-empty container-id. An empty value is rejected with CLOSE(amqp:invalid-field, "open.container-id is required"). The value is sanitized ([a-zA-Z0-9_-], ≤ 256).

container-id matters for two reasons beyond identity:

  • It becomes the ClientID whenever there is no SASL identity (ANONYMOUS / bare / auth-off PLAIN-with-empty-username).
  • It is half of the durable-subscription identity. A durable events-store subscriber that reconnects with a different container-id will not resume its old position — it becomes a different durable identity.

Set a stable container-id for any durable subscriber. Because the container-id is half the durable identity, a subscriber that lets its container-id drift across reconnects (e.g. a randomly generated one) will never resume — it creates a new subscription each time. See Reliability.

Authorization — enforced at attach, per resource

Once authenticated, the connector enforces a Casbin policy at ATTACH time, against the ClientID, the resolved pattern (mapped to a resource name), the channel, and the link role. If no authorizer is wired (auth off), nothing is enforced.

Link the client attachesServer rolePermission enforced
Receiver from <pattern>/<ch> (client consumes)server-senderRead on (ClientID, resource, channel)
Sender to a fixed <pattern>/<ch> (client produces)server-receiverWrite on (ClientID, resource, channel)
Sender with a null (anonymous) targetserver-receiverdeferred — each message is authorized per-message with Write on its properties.to
/responses/<RequestID> (RPC reply token)server-receivernot enforced — connection-scoped reply token
  • The resource name maps the pattern: events-storeevents_store; queues / events / commands / queries map to themselves.
  • Anonymous-terminus links cannot be checked at attach because there is no fixed channel yet. Instead, the transfer layer authorizes each message with a Write check on the message's to, backed by a short-lived LRU cache. See Addressing.

A denied authorization closes the link with DETACH(amqp:unauthorized-access) and a generic description. No policy internals leak. Your client should surface the amqp:unauthorized-access condition and treat it as a permission error, not retry it as transient.

Audit events — exactly two

The connector's audit surface for AMQP 1.0 lives entirely in the SASL layer and emits only two event types:

Audit eventEmitted onFields
auth.successsuccessful SASL authenticationClientID, Transport: "amqp10", SourceIP, Metadata{mechanism}
auth.failurefailed/rejected SASL authenticationClientID, Transport: "amqp10", SourceIP, Error (sanitized), Metadata{mechanism}

The AMQP 1.0 connector does not emit client.connected or client.disconnected audit events. Do not build alerting, dashboards, or compliance reporting that depends on connection-lifecycle audit events from this connector — they are not produced. The only audit signal is authentication success/failure (with the SASL mechanism and source IP). For connection/link visibility, use the dashboard API and Prometheus metrics — see Connections endpoint.

Quick decision guide

You want…Do this
Clone-and-run on a stock dev brokerConnect ANONYMOUS (no credentials)
Authenticate with KubeMQ identitySASL PLAIN, JWT in the password slot; username is audit-only
Authenticate with a client certificatemTLS + SASL EXTERNAL (cert CN → ClientID) — see TLS and mTLS
Resume a durable events-store subscriptionSet a stable container-id (it is half the durable identity)
Diagnose a permission failureLook for DETACH(amqp:unauthorized-access); check the Read/Write policy for that channel

Was this page helpful?

On this page