Feature/3902 rabbitmq mutual tls#3953
Conversation
Implements client certificate configuration for RabbitMQ connections
to support mutual TLS authentication.
Changes:
- Add ClientCertificate, ClientCertificatePath, and
ClientCertificatePassword properties to RmqMessagingGatewayConnection
- Implement SSL configuration in RmqMessageGateway for both sync and
async variants
- Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format)
- Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors)
- Certificate object takes precedence over file path
- Maintain backwards compatibility (certificate configuration is optional)
Testing:
- Behavioral tests verify ConnectionFactory.Ssl configuration
- Tests assert SSL enablement, certificate passing, and precedence
- Error case tests (missing file, invalid certificate)
- Self-signed certificates generated in test setup
- Full test coverage for both sync and async variants
- All 14 tests passing
Compliance with Critical Guidelines:
- Rule BrighterCommand#1: Implemented identically for both sync AND async variants
- Rule BrighterCommand#3: Backwards compatible (certificate is optional, all nullable)
- Rule BrighterCommand#7: Used typed X509Certificate2 API consistently
- Rule BrighterCommand#9: Extracted LoadCertificate helper method
- Rules BrighterCommand#10-12: No changes to trace propagation (observability preserved)
Implements mutual TLS (mTLS) authentication for RabbitMQ connections,
enabling secure communication where both client and server authenticate
using X.509 certificates.
Changes:
- Add ClientCertificate, ClientCertificatePath, and ClientCertificatePassword
properties to RmqMessagingGatewayConnection (Sync & Async)
- Add TrustServerSelfSignedCertificate property for test/dev environments
- Configure SSL options with certificate chain validation
- Fix GetSanitizedUri() to handle mTLS URIs without username/password
- Use NET9_0_OR_GREATER conditional compilation for X509CertificateLoader
(falls back to X509Certificate2 for .NET 8.0 and netstandard2.0)
- Add acceptance tests with [Trait("Requires", "Docker-mTLS")] for CI filtering
The implementation provides full parity between Sync (RMQ v6) and Async
(RMQ v7) gateways. Certificate loading supports both runtime objects and
file paths. Configuration is secure by default with opt-in flag for
accepting self-signed certificates in test environments only.
Verifies that W3C Trace Context (TraceParent, TraceState, Baggage) and CloudEvents trace context survive mTLS connections, ensuring compliance with Critical Review Guidelines Rules BrighterCommand#10-12. Tests verify: - TraceParent header preservation over mTLS - TraceState and Baggage propagation - BrighterTracer.WriteProducerEvent instrumentation - CloudEvents trace context serialization - Parity between Sync and Async gateways Tagged with [Trait("Category", "Observability")] for selective test execution.
Creates a minimal Web API that demonstrates publishing and consuming messages over RabbitMQ with mutual TLS authentication. Features: - Single application acts as both producer and consumer - TodoCreated event with TodoCreatedHandler - REST endpoints: POST /todos and GET /health - RabbitMQ connection configured with client certificate - Comprehensive README with setup and troubleshooting - Built for .NET 8.0 compatibility The test harness verifies end-to-end mTLS functionality including certificate loading, SSL connection establishment, message publishing with publisher confirms, and message consumption over secure connection.
The mTLS tests require:
- Generated certificates (tests/certs/)
- RabbitMQ with mTLS configuration
- Docker compose setup with special config
These tests are tagged with [Trait("Requires", "Docker-mTLS")] and
should only run when explicitly requested with --filter "Requires=Docker-mTLS",
not in the regular rabbitmq-ci workflow.
The observability test classes were missing the [Trait("Requires", "Docker-mTLS")]
attribute, causing them to run in regular CI despite needing mTLS infrastructure.
This trait ensures these tests are only executed when explicitly requested
with --filter "Requires=Docker-mTLS", matching the acceptance tests.
Add Collection attributes to sync mTLS test classes to prevent parallel execution race conditions during TLS handshake.
Changes:
- Add [Collection("RabbitMQ mTLS")] to mTLS test classes
- Add RabbitMQMtlsTestCollection with DisableParallelization
- Add quorum queue test coverage for both sync and async
All 18 mTLS tests (9 sync + 9 async) now pass consistently.
fca9146 to
a8ac178
Compare
|
Likely back on Brighter stuff tomorrow |
The AddUserDefinedHeaders method in the SYNC version (RmqMessagePublisher.cs)
was missing the negation operator that exists in the ASYNC version. This caused
only system headers to be copied during message requeuing instead of user-defined
headers, resulting in message body loss.
Changed:
if (_headersToReset.Contains(header.Key))
To:
if (!_headersToReset.Contains(header.Key))
This brings the SYNC implementation in line with the ASYNC version from
commit 76aeced.
|
@darrenschwarz I am done commenting on this, for now, if you want to move it on. |
- Update copyright attribution to Darren Schwarz - Remove excessive XML documentation from internal methods - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate folders - Add PowerShell certificate generation script for Windows - Revert race condition fix (will be addressed separately) - Update documentation with boolean logic bug fix explanation
- Update copyright attribution to Darren Schwarz per CLA - Remove ALL XML documentation from internal RmqTlsConfigurator classes - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate Acceptance/ folders - Add PowerShell certificate generation script for Windows developers - Revert race condition fix (will be addressed in separate issue) - Update namespaces for moved test files All review comments from @iancooper addressed. Unit tests: 14/14 passed Acceptance tests: 18/18 passed Total: 32/32 mTLS tests passing
- Update copyright attribution to Darren Schwarz per CLA - Remove ALL XML documentation from internal RmqTlsConfigurator classes - Remove license headers from test files per project convention - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate Acceptance/ folders - Add PowerShell certificate generation script for Windows developers - Revert race condition fix (will be addressed in separate issue) - Update namespaces for moved test files All review comments from @iancooper addressed. Unit tests: 14/14 passed Acceptance tests: 18/18 passed Total: 32/32 mTLS tests passing
All comments have been addressed, ready for review. |
There was a problem hiding this comment.
Gates Passed
4 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: Clean Code Collective
Want more control? Customize Code Health rules or catch issues early with our IDE extension and CLI tool.
| @@ -0,0 +1,25 @@ | |||
| { | |||
There was a problem hiding this comment.
Thanks for adding the samples, we appreciate it when new features come with examples of how to use them
| message.Header.Bag.Each(header => | ||
| { | ||
| if (_headersToReset.Contains(header.Key)) | ||
| if (!_headersToReset.Contains(header.Key)) |
There was a problem hiding this comment.
Is this still reversing, though? The logic is that we only want to reset certain headers from a list. If they are in the list, we overwrite them. If we use the ! operator, this means reset if the list does not contain the header.
|
@darrenschwarz Even I am getting confused as to whether the sign reversal was fixed now |
So the fix here is correct, unless I misunderstand the intention. This will ensure that we respect any user defined headers, and we are checking against the static system/blocked list of headers (_headersToReset). I'll run through with you when we catchup. If the fix is good, then I was thinking that we might want to do some minor renaming to make intention more apparent. |
I'll take a look. I need to refresh my memory of why we reset some headers. Arguably the fix is out-of-scope for a change to mTLS. Now, human authors do this, because sometimes we stumble over a bug, and need to fix it to progress. I guess, when reviewing an agent's code, it's harder to see intent - why did you change this? - why did it help your code work. |
This was actually a human in the loop instance, I cam across it because tests were failing. :) |
I'm happy to revert this and create another issue, and submit the fix as an isolated item. |
Nah, all good. Only gave the feedback in case it was an agent |
* Add mutual TLS support for RabbitMQ (BrighterCommand#3902) Implements client certificate configuration for RabbitMQ connections to support mutual TLS authentication. Changes: - Add ClientCertificate, ClientCertificatePath, and ClientCertificatePassword properties to RmqMessagingGatewayConnection - Implement SSL configuration in RmqMessageGateway for both sync and async variants - Support X509Certificate2 objects and file paths (.pfx/PKCS#12 format) - Use X509CertificateLoader.LoadPkcs12FromFile (not obsolete constructors) - Certificate object takes precedence over file path - Maintain backwards compatibility (certificate configuration is optional) Testing: - Behavioral tests verify ConnectionFactory.Ssl configuration - Tests assert SSL enablement, certificate passing, and precedence - Error case tests (missing file, invalid certificate) - Self-signed certificates generated in test setup - Full test coverage for both sync and async variants - All 14 tests passing Compliance with Critical Guidelines: - Rule #1: Implemented identically for both sync AND async variants - Rule #3: Backwards compatible (certificate is optional, all nullable) - Rule #7: Used typed X509Certificate2 API consistently - Rule #9: Extracted LoadCertificate helper method - Rules #10-12: No changes to trace propagation (observability preserved) * Add RabbitMQ mutual TLS support with self-signed certificate option Implements mutual TLS (mTLS) authentication for RabbitMQ connections, enabling secure communication where both client and server authenticate using X.509 certificates. Changes: - Add ClientCertificate, ClientCertificatePath, and ClientCertificatePassword properties to RmqMessagingGatewayConnection (Sync & Async) - Add TrustServerSelfSignedCertificate property for test/dev environments - Configure SSL options with certificate chain validation - Fix GetSanitizedUri() to handle mTLS URIs without username/password - Use NET9_0_OR_GREATER conditional compilation for X509CertificateLoader (falls back to X509Certificate2 for .NET 8.0 and netstandard2.0) - Add acceptance tests with [Trait("Requires", "Docker-mTLS")] for CI filtering The implementation provides full parity between Sync (RMQ v6) and Async (RMQ v7) gateways. Certificate loading supports both runtime objects and file paths. Configuration is secure by default with opt-in flag for accepting self-signed certificates in test environments only. * Add observability tests for RabbitMQ mutual TLS Verifies that W3C Trace Context (TraceParent, TraceState, Baggage) and CloudEvents trace context survive mTLS connections, ensuring compliance with Critical Review Guidelines Rules #10-12. Tests verify: - TraceParent header preservation over mTLS - TraceState and Baggage propagation - BrighterTracer.WriteProducerEvent instrumentation - CloudEvents trace context serialization - Parity between Sync and Async gateways Tagged with [Trait("Category", "Observability")] for selective test execution. * Add simple mTLS test harness for RabbitMQ Creates a minimal Web API that demonstrates publishing and consuming messages over RabbitMQ with mutual TLS authentication. Features: - Single application acts as both producer and consumer - TodoCreated event with TodoCreatedHandler - REST endpoints: POST /todos and GET /health - RabbitMQ connection configured with client certificate - Comprehensive README with setup and troubleshooting - Built for .NET 8.0 compatibility The test harness verifies end-to-end mTLS functionality including certificate loading, SSL connection establishment, message publishing with publisher confirms, and message consumption over secure connection. * Exclude mTLS tests from regular CI The mTLS tests require: - Generated certificates (tests/certs/) - RabbitMQ with mTLS configuration - Docker compose setup with special config These tests are tagged with [Trait("Requires", "Docker-mTLS")] and should only run when explicitly requested with --filter "Requires=Docker-mTLS", not in the regular rabbitmq-ci workflow. * Add Requires=Docker-mTLS trait to observability tests The observability test classes were missing the [Trait("Requires", "Docker-mTLS")] attribute, causing them to run in regular CI despite needing mTLS infrastructure. This trait ensures these tests are only executed when explicitly requested with --filter "Requires=Docker-mTLS", matching the acceptance tests. * Fix sync mTLS test failures by enforcing sequential execution Add Collection attributes to sync mTLS test classes to prevent parallel execution race conditions during TLS handshake. Changes: - Add [Collection("RabbitMQ mTLS")] to mTLS test classes - Add RabbitMQMtlsTestCollection with DisableParallelization - Add quorum queue test coverage for both sync and async All 18 mTLS tests (9 sync + 9 async) now pass consistently. * deleted local certs for testing * Fix missing NOT operator in RMQ sync requeue logic The AddUserDefinedHeaders method in the SYNC version (RmqMessagePublisher.cs) was missing the negation operator that exists in the ASYNC version. This caused only system headers to be copied during message requeuing instead of user-defined headers, resulting in message body loss. Changed: if (_headersToReset.Contains(header.Key)) To: if (!_headersToReset.Contains(header.Key)) This brings the SYNC implementation in line with the ASYNC version from commit 76aeced. * Fix mTLS certificate validation and race condition in sync consumer This commit includes fixes to complete the mTLS implementation for RabbitMQ messaging gateways, resolving test failures and a race condition. ## mTLS Implementation - Added RmqTlsConfigurator helper classes (Async and Sync variants) to centralize TLS/SSL configuration logic - Extracts certificate loading and SslOption configuration into reusable internal static class - Supports both X509Certificate2 objects and file paths with optional passwords - Integrates with RmqMessagingGatewayConnection configuration ## Bug Fixes 1. Certificate validation failures (18 tests) - Server certificate is issued for hostname rabbitmq-mtls but tests connect to localhost, causing SSL name mismatch errors - Updated RmqTlsConfigurator to accept both RemoteCertificateChainErrors AND RemoteCertificateNameMismatch when TrustServerSelfSignedCertificate is enabled - This is appropriate for test/development environments with self-signed certificates 2. Race condition in sync consumer (intermittent failures) - RmqMessageConsumer.CreateConsumer() was manually calling HandleBasicConsumeOk after BasicConsume - RabbitMQ client already calls this method automatically as a callback, causing concurrent modifications to non-thread-safe collections in DefaultBasicConsumer - Removed the redundant manual call and added explanatory comment All 32 mTLS tests now pass consistently (16 async + 16 sync). Files added: - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cs - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cs Files modified: - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessageGateway.cs - src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessagingGatewayConnection.cs - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageGateway.cs - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessagingGatewayConnection.cs - src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageConsumer.cs Fixes BrighterCommand#3902 * Address PR review comments - Update copyright attribution to Darren Schwarz - Remove excessive XML documentation from internal methods - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate folders - Add PowerShell certificate generation script for Windows - Revert race condition fix (will be addressed separately) - Update documentation with boolean logic bug fix explanation * Address PR review comments - Update copyright attribution to Darren Schwarz per CLA - Remove ALL XML documentation from internal RmqTlsConfigurator classes - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate Acceptance/ folders - Add PowerShell certificate generation script for Windows developers - Revert race condition fix (will be addressed in separate issue) - Update namespaces for moved test files All review comments from @iancooper addressed. Unit tests: 14/14 passed Acceptance tests: 18/18 passed Total: 32/32 mTLS tests passing * Address PR review comments - Update copyright attribution to Darren Schwarz per CLA - Remove ALL XML documentation from internal RmqTlsConfigurator classes - Remove license headers from test files per project convention - Replace Guid.NewGuid().ToString() with Id.Random() (V10 pattern) - Reorganize acceptance tests into separate Acceptance/ folders - Add PowerShell certificate generation script for Windows developers - Revert race condition fix (will be addressed in separate issue) - Update namespaces for moved test files All review comments from @iancooper addressed. Unit tests: 14/14 passed Acceptance tests: 18/18 passed Total: 32/32 mTLS tests passing --------- Co-authored-by: Ian Cooper <ian_hammond_cooper@yahoo.co.uk>
Add mutual TLS (mTLS) support to RabbitMQ messaging gateways
Fixes #3902
Summary
Adds mutual TLS (mTLS) authentication support for RabbitMQ connections in both the Sync (RMQ v6) and Async (RMQ v7) messaging gateways.
Changes
Core Implementation
Added client certificate configuration to
RmqMessagingGatewayConnection:ClientCertificate- X509Certificate2 object (for programmatic configuration)ClientCertificatePath- File path to .pfx/PKCS#12 certificate (for file-based configuration)ClientCertificatePassword- Optional password for certificate filesTrustServerSelfSignedCertificate- Opt-in flag for test/dev environments (follows SQL Server naming convention)New TLS Configuration Helpers:
RmqTlsConfiguratorclasses (Async and Sync variants) to centralize TLS/SSL configuration logicSslOptionconfiguration into reusable internal static classesX509Certificate2) and .NET 9.0+ (X509CertificateLoader)RmqMessageGatewayviaRmqTlsConfigurator.ConfigureIfEnabled()callFixed
GetSanitizedUri()to handle mTLS URIs that don't contain username/password credentials (adds early return for URIs without @ symbol; existing username/password sanitization logic unchanged and backward compatible)Cross-Gateway Parity
Both Sync and Async gateways have identical mTLS implementations, maintaining the established pattern of feature parity between RMQ v6 and v7 clients.
Bug Fixes
Pre-existing RabbitMQ Bugs (Discovered During Testing)
While testing the mTLS implementation, CI revealed pre-existing bugs in the RabbitMQ message requeuing logic:
1. Missing NOT operator in AddUserDefinedHeaders (SYNC version)
AddUserDefinedHeaderswas missing the!negation operator that exists in the ASYNC versionif (_headersToReset.Contains(header.Key))toif (!_headersToReset.Contains(header.Key))These fixes bring the SYNC implementation in line with the ASYNC version.
mTLS Implementation Bugs (Fixed in This PR)
1. Certificate Validation Hostname Mismatch
AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallbackRmqTlsConfigurator(both Async and Sync) to accept bothRemoteCertificateChainErrorsANDRemoteCertificateNameMismatchwhenTrustServerSelfSignedCertificateis enabledsrc/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cs:60-61,src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cs:60-612. Race Condition in Sync Consumer
RmqMessageConsumer.CreateConsumer()was manually callingHandleBasicConsumeOk()afterBasicConsume(), but RabbitMQ client already calls this method automatically as a callbackInvalidOperationException: Operations that change non-concurrent collections must have exclusive accessdue to concurrent modifications to non-thread-safeHashSetinDefaultBasicConsumerHandleBasicConsumeOk()and added explanatory commentsrc/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageConsumer.cs:380-381Testing
All 32 mTLS tests (16 async + 16 sync) pass consistently.
Quick Test Commands (3 minutes total)
Run all 32 mTLS tests (most efficient):
OR run separately by category:
Why These Commands Are Fast
--framework net10.0instead of testing on net8.0, net9.0, net10.0Test Coverage
Added comprehensive test coverage across four categories:
Configuration Tests (14): Verify SSL plumbing, certificate loading edge cases, and validation logic
Acceptance Tests (4): Verify actual mTLS connections against Docker RabbitMQ
Observability Tests (10): Verify W3C Trace Context (TraceParent, TraceState, Baggage) and CloudEvents trace context survive mTLS connections
Quorum Queue Tests (4): Verify mTLS works with RabbitMQ quorum queues
All tests include both async and sync variants to ensure cross-gateway uniformity.
Test Stability
[Collection("RabbitMQ mTLS", DisableParallelization = true)]to prevent TLS handshake race conditions[Trait("Requires", "Docker-mTLS")]for selective executionInfrastructure
tests/docker-compose.rabbitmq-mtls.yml)tests/rabbitmq-mtls.conf)tests/generate-test-certs.sh) for creating test CA, server, and client certificates.gitignoreto exclude generated test certificatesTest Harness
Added a simple Web API sample in
samples/WebAPI/WebAPI_mTLS_TestHarness/that demonstrates:This provides a working example of the mTLS configuration and verifies end-to-end functionality.
Usage Example
Files Changed
Added
src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cssrc/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cstests/docker-compose.rabbitmq-mtls.ymltests/rabbitmq-mtls.conftests/generate-test-certs.shsamples/WebAPI/WebAPI_mTLS_TestHarness/(manual test harness)Modified
src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessageGateway.cssrc/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessagingGatewayConnection.cssrc/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageGateway.cssrc/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessagingGatewayConnection.cssrc/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageConsumer.csNotes
TrustServerSelfSignedCertificatedefaults tofalsefor production safetySecurity Considerations
TrustServerSelfSignedCertificate = trueshould only be used in test/development environmentsTrustServerSelfSignedCertificate = false)Breaking Changes
None. This is a purely additive feature. Existing code without mTLS configuration continues to work as before.
Known Issue
There is an intermittent test failure on .NET 10.0 only in CI:
RmqMessageProducerRequeuingMessageTests.When_posting_a_message_via_the_messaging_gatewayThis appears to be a .NET 10.0-specific timing/environmental issue that requires further investigation. The core bug fix (missing
!operator) is correct and working.Bug Fix: Corrected header filtering logic in sync RmqMessagePublisher
This PR includes a critical bug fix for message header handling that was discovered during mTLS testing.
Issue: In commit
76aeced5c(September 2025), a refactoring of the syncRmqMessagePublisheraccidentally inverted the header filtering logic in theAddUserDefinedHeadersmethod:Impact: This caused only Brighter's internal system headers (MESSAGE_TYPE, TOPIC, HANDLED_COUNT, DELIVERY_TAG, CORRELATION_ID) to be copied during message publishing and requeuing, while user-defined headers were discarded. This resulted in message body loss and data corruption during message requeuing operations.
Fix: Restored the correct logic by adding back the NOT operator:
This brings the sync implementation in line with the async version (
src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqMessagePublisher.cs:231), which retained the correct logic throughout the refactoring.Discovery: This bug was discovered during mTLS acceptance testing when messages were being requeued and custom headers were not preserved, causing test failures.
Files Changed:
src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessagePublisher.cs(line 197)Note on Race Condition Fix
The race condition fix in
RmqMessageConsumer.CreateConsumer()(removal of manualHandleBasicConsumeOk()call) has been reverted from this PR and will be addressed in a separate issue for better scope management. See RACE_CONDITION_ISSUE.md for details on creating the tracking issue.