TLS parameters example¶
This example demonstrates a TLS session with RabbitMQ using mutual authentication (server and client authentication).
See the RabbitMQ TLS/SSL documentation for certificate generation and RabbitMQ TLS configuration.
Both examples on this page read the certificates in tests/certs/, which are the ones the repository's own CI configuration gives the broker, so they run against a broker started with that configuration:
hatch run rabbitmq-tls
examples/tls_mutual_authentication.py:
import logging
import ssl
from pathlib import Path
import pika
logging.basicConfig(level=logging.INFO)
# The certificates the test suite uses, so this runs against the broker the
# repository's own CI configuration starts. See `.ci/linux/rabbitmq.conf`.
_certs = Path(__file__).resolve().parent.parent / 'tests' / 'certs'
context = ssl.create_default_context(cafile=str(_certs / 'ca_certificate.pem'))
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(str(_certs / 'client_certificate.pem'),
str(_certs / 'client_key.pem'))
ssl_options = pika.SSLOptions(context, 'localhost')
conn_params = pika.ConnectionParameters(port=5671, ssl_options=ssl_options)
with pika.BlockingConnection(conn_params) as conn:
ch = conn.channel()
# `durable=True` is required by RabbitMQ 4.3; see examples/README.md.
ch.queue_declare('foobar', durable=True)
ch.basic_publish('', 'foobar', b'Hello, world!')
print(ch.basic_get('foobar'))
That configuration, .ci/linux/rabbitmq.conf, included here in full so it cannot drift from the file:
listeners.tcp.default = 5672
listeners.ssl.default = 5671
num_acceptors.tcp = 10
num_acceptors.ssl = 10
reverse_dns_lookups = false
loopback_users.guest = false
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = true
ssl_options.cacertfile = /etc/rabbitmq/certs/ca_certificate.pem
ssl_options.certfile = /etc/rabbitmq/certs/server_certificate.pem
ssl_options.keyfile = /etc/rabbitmq/certs/server_key.pem
log.console = true
log.console.level = debug
Note ssl_options.fail_if_no_peer_cert = true, which requires every TLS client to present a certificate chain. Mutual authentication is therefore the only thing this broker allows over TLS; the same configuration still serves plaintext on 5672, which is what the rest of the examples use. If what you want is server authentication alone, verifying the broker and presenting nothing of your own, set fail_if_no_peer_cert = false and drop the load_cert_chain call; everything else above stays as it is.
To use your own certificates instead of the test ones, generate a set with tls-gen and point both the configuration and the example at them. See configure for where rabbitmq.conf lives on your system.
The example above authenticates as guest over PLAIN and needs nothing more than that. Authenticating as the client certificate instead is a separate feature: it needs the RabbitMQ TLS (x509 certificate) authentication mechanism enabled, EXTERNAL among the broker's auth_mechanisms, and a user named for the certificate's distinguished name. The Twisted example below does that, and its docstring spells out the requirements:
"""
Mutual TLS with the Twisted adapter, authenticating by client certificate.
`ExternalCredentials` asks the broker for SASL EXTERNAL, which means the broker takes the identity
from the TLS client certificate rather than from a username and password. That needs broker-side
setup beyond a TLS listener:
- the `rabbitmq_auth_mechanism_ssl` plugin enabled
- `EXTERNAL` among `auth_mechanisms`
- a user named for the identity the broker extracts from the certificate. It uses the whole
distinguished name by default, so for `tests/certs/client_certificate.pem` that user is
`O=client,CN=shostakovich`; set `ssl_cert_login_from = common_name` to have it use `shostakovich`
instead
Without those the connection is refused during authentication. See
https://www.rabbitmq.com/docs/access-control#with-client-certificate-authentication
"""
import sys
from pathlib import Path
from twisted.internet import defer, protocol, reactor, ssl
from pika import ConnectionParameters
from pika.adapters import twisted_connection
from pika.credentials import ExternalCredentials
@defer.inlineCallbacks
def publish(connection):
channel = yield connection.channel()
yield channel.basic_publish(
exchange='amq.topic',
routing_key='hello.world',
body='Hello World!',
)
print('published')
def connection_ready(conn):
conn.ready.addCallback(lambda _: conn)
return conn.ready
def done(_result):
reactor.stop()
failures = []
def failed(failure):
# Without this the reactor reports 'Unhandled error in Deferred' and the
# process sits there, which says nothing about the broker refusing the
# certificate.
print(f'failed: {failure.type.__name__}: {failure.getErrorMessage()}')
failures.append(failure)
reactor.stop()
_certs = Path(__file__).resolve().parent.parent / 'tests' / 'certs'
# Load the CA certificate to validate the server's identity
with _certs.joinpath('ca_certificate.pem').open() as fd:
ca_cert = ssl.Certificate.loadPEM(fd.read())
# Load the client certificate and key to authenticate with the server
with _certs.joinpath('client_key.pem').open() as fd:
client_key = fd.read()
with _certs.joinpath('client_certificate.pem').open() as fd:
client_cert = fd.read()
client_keypair = ssl.PrivateCertificate.loadPEM(client_key + client_cert)
context_factory = ssl.optionsForClientTLS(
'localhost',
trustRoot=ca_cert,
clientCertificate=client_keypair,
)
params = ConnectionParameters(credentials=ExternalCredentials())
cc = protocol.ClientCreator(reactor,
twisted_connection.TwistedProtocolConnection,
params)
deferred = cc.connectSSL('localhost', 5671, context_factory)
deferred.addCallback(connection_ready)
deferred.addCallback(publish)
deferred.addCallbacks(done, failed)
reactor.run()
sys.exit(1 if failures else 0)