Skip to content

Feature/3902 rabbitmq mutual tls#3953

Merged
iancooper merged 14 commits into
BrighterCommand:masterfrom
darrenschwarz:feature/3902-rabbitmq-mutual-tls
Feb 1, 2026
Merged

Feature/3902 rabbitmq mutual tls#3953
iancooper merged 14 commits into
BrighterCommand:masterfrom
darrenschwarz:feature/3902-rabbitmq-mutual-tls

Conversation

@darrenschwarz

@darrenschwarz darrenschwarz commented Dec 28, 2025

Copy link
Copy Markdown
Contributor

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 files
  • TrustServerSelfSignedCertificate - Opt-in flag for test/dev environments (follows SQL Server naming convention)

New TLS Configuration Helpers:

  • Added RmqTlsConfigurator classes (Async and Sync variants) to centralize TLS/SSL configuration logic
  • Extracts certificate loading and SslOption configuration into reusable internal static classes
  • Supports both X509Certificate2 objects and file paths with optional passwords
  • Includes conditional compilation to support both .NET 8.0 (X509Certificate2) and .NET 9.0+ (X509CertificateLoader)
  • Integrated with RmqMessageGateway via RmqTlsConfigurator.ConfigureIfEnabled() call

Fixed 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)

  • Issue: The SYNC version of AddUserDefinedHeaders was missing the ! negation operator that exists in the ASYNC version
  • Impact: Only system headers were being copied during requeue instead of user-defined headers, causing message body loss
  • Fix: Changed if (_headersToReset.Contains(header.Key)) to if (!_headersToReset.Contains(header.Key))
  • Commit: 15acf14

These fixes bring the SYNC implementation in line with the ASYNC version.

mTLS Implementation Bugs (Fixed in This PR)

1. Certificate Validation Hostname Mismatch

  • Issue: Server certificate is issued for hostname "rabbitmq-mtls" but tests connect to "localhost", causing SSL name mismatch errors
  • Impact: All 18 Docker-dependent mTLS tests failed with AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallback
  • Fix: Updated RmqTlsConfigurator (both Async and Sync) to accept both RemoteCertificateChainErrors AND RemoteCertificateNameMismatch when TrustServerSelfSignedCertificate is enabled
  • Files: src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cs:60-61, src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cs:60-61

2. Race Condition in Sync Consumer

  • Issue: RmqMessageConsumer.CreateConsumer() was manually calling HandleBasicConsumeOk() after BasicConsume(), but RabbitMQ client already calls this method automatically as a callback
  • Impact: Intermittent InvalidOperationException: Operations that change non-concurrent collections must have exclusive access due to concurrent modifications to non-thread-safe HashSet in DefaultBasicConsumer
  • Fix: Removed the redundant manual call to HandleBasicConsumeOk() and added explanatory comment
  • Files: src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqMessageConsumer.cs:380-381

Testing

All 32 mTLS tests (16 async + 16 sync) pass consistently.

Quick Test Commands (3 minutes total)

Run all 32 mTLS tests (most efficient):

dotnet test tests/Paramore.Brighter.RMQ.Async.Tests/Paramore.Brighter.RMQ.Async.Tests.csproj --filter "Category=MutualTLS" --framework net10.0 && \
dotnet test tests/Paramore.Brighter.RMQ.Sync.Tests/Paramore.Brighter.RMQ.Sync.Tests.csproj --filter "Category=MutualTLS" --framework net10.0

OR run separately by category:

# 1. Unit/configuration tests only (14 tests, no Docker required, ~5 seconds)
dotnet test tests/Paramore.Brighter.RMQ.Async.Tests/Paramore.Brighter.RMQ.Async.Tests.csproj \
  --filter "Category=MutualTLS&Requires!=Docker-mTLS" --framework net10.0

dotnet test tests/Paramore.Brighter.RMQ.Sync.Tests/Paramore.Brighter.RMQ.Sync.Tests.csproj \
  --filter "Category=MutualTLS&Requires!=Docker-mTLS" --framework net10.0

# 2. Acceptance/observability tests (18 tests, requires Docker, ~3 minutes)
# First time setup:
cd tests
./generate-test-certs.sh
docker-compose -f docker-compose.rabbitmq-mtls.yml up -d
cd ..

# Run Docker-dependent tests:
dotnet test tests/Paramore.Brighter.RMQ.Async.Tests/Paramore.Brighter.RMQ.Async.Tests.csproj \
  --filter "Requires=Docker-mTLS" --framework net10.0

dotnet test tests/Paramore.Brighter.RMQ.Sync.Tests/Paramore.Brighter.RMQ.Sync.Tests.csproj \
  --filter "Requires=Docker-mTLS" --framework net10.0

Why These Commands Are Fast

  • Targets specific test projects - Only builds RMQ-related projects (not the entire solution)
  • Single framework - Uses --framework net10.0 instead of testing on net8.0, net9.0, net10.0
  • Precise filtering - Only runs the 32 mTLS tests, not all RMQ tests
  • 3 minutes vs 3-5 hours - Avoids building superfluous projects

Test Coverage

Added comprehensive test coverage across four categories:

Configuration Tests (14): Verify SSL plumbing, certificate loading edge cases, and validation logic

  • Certificate object configuration
  • File path configuration with passwords
  • Invalid certificate handling
  • Missing file error handling
  • SSL option configuration

Acceptance Tests (4): Verify actual mTLS connections against Docker RabbitMQ

  • Basic publish with client certificate
  • Round-trip publish and consume

Observability Tests (10): Verify W3C Trace Context (TraceParent, TraceState, Baggage) and CloudEvents trace context survive mTLS connections

  • TraceParent header preservation
  • TraceState and Baggage preservation
  • BrighterTracer.WriteProducerEvent integration
  • CloudEvents trace context serialization

Quorum Queue Tests (4): Verify mTLS works with RabbitMQ quorum queues

  • Trace context with quorum queues
  • Baggage with quorum queues

All tests include both async and sync variants to ensure cross-gateway uniformity.

Test Stability

  • Tests use [Collection("RabbitMQ mTLS", DisableParallelization = true)] to prevent TLS handshake race conditions
  • Docker-dependent tests are marked with [Trait("Requires", "Docker-mTLS")] for selective execution
  • All tests pass consistently on .NET 8.0, 9.0, and 10.0

Infrastructure

  • Added Docker Compose configuration for RabbitMQ with mTLS enabled (tests/docker-compose.rabbitmq-mtls.yml)
  • Added RabbitMQ mTLS configuration file (tests/rabbitmq-mtls.conf)
  • Added certificate generation script (tests/generate-test-certs.sh) for creating test CA, server, and client certificates
  • Updated .gitignore to exclude generated test certificates

Test Harness

Added a simple Web API sample in samples/WebAPI/WebAPI_mTLS_TestHarness/ that demonstrates:

  • Publishing and consuming messages over RabbitMQ with mTLS
  • Both producer and consumer in a single application
  • REST API with Swagger UI for easy testing

This provides a working example of the mTLS configuration and verifies end-to-end functionality.

Usage Example

// Option 1: Provide certificate object directly
var cert = new X509Certificate2("client-cert.pfx", "password");
var connection = new RmqMessagingGatewayConnection
{
    AmpqUri = new AmqpUriSpecification(new Uri("amqps://rabbitmq.example.com:5671")),
    Exchange = new Exchange("my.exchange"),
    ClientCertificate = cert,
    TrustServerSelfSignedCertificate = true // For dev/test only
};

// Option 2: Load from file path
var connection = new RmqMessagingGatewayConnection
{
    AmpqUri = new AmqpUriSpecification(new Uri("amqps://rabbitmq.example.com:5671")),
    Exchange = new Exchange("my.exchange"),
    ClientCertificatePath = "/path/to/client-cert.pfx",
    ClientCertificatePassword = "password",
    TrustServerSelfSignedCertificate = false // Production: validate properly
};

// Use with producer or consumer as normal
var producer = new RmqMessageProducer(connection);
await producer.SendAsync(message);

Files Changed

Added

  • src/Paramore.Brighter.MessagingGateway.RMQ.Async/RmqTlsConfigurator.cs
  • src/Paramore.Brighter.MessagingGateway.RMQ.Sync/RmqTlsConfigurator.cs
  • tests/docker-compose.rabbitmq-mtls.yml
  • tests/rabbitmq-mtls.conf
  • tests/generate-test-certs.sh
  • 6 test files with 32 comprehensive test cases
  • samples/WebAPI/WebAPI_mTLS_TestHarness/ (manual test harness)

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

Notes

  • Certificate configuration is fully backward compatible - existing code continues to work unchanged
  • TrustServerSelfSignedCertificate defaults to false for production safety
  • Certificate object takes precedence over file path if both are configured
  • Tests use trait-based filtering rather than environment variables per feedback on ADR

Security Considerations

  • TrustServerSelfSignedCertificate = true should only be used in test/development environments
  • In production, use properly signed certificates and validate the full certificate chain (TrustServerSelfSignedCertificate = false)
  • Certificate passwords should be stored securely (e.g., Azure Key Vault, environment variables, not hardcoded)
  • Certificates are loaded securely using the appropriate .NET APIs for the target framework

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_gateway
  • Passes consistently on .NET 9.0
  • ASYNC version passes on both .NET 9.0 and 10.0
  • SYNC now has identical logic to ASYNC

This 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 sync RmqMessagePublisher accidentally inverted the header filtering logic in the AddUserDefinedHeaders method:

// BEFORE (correct):
if (!_headersToReset.Any(htr => htr.Equals(header.Key)))
    headers.Add(header.Key, header.Value);

// AFTER (incorrect - missing NOT operator):
if (_headersToReset.Contains(header.Key))
{
    headers[header.Key] = header.Value;
}

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:

// CORRECTED:
if (!_headersToReset.Contains(header.Key))
{
    headers[header.Key] = header.Value;
}

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 manual HandleBasicConsumeOk() 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.


codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

   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.
@darrenschwarz darrenschwarz force-pushed the feature/3902-rabbitmq-mutual-tls branch from fca9146 to a8ac178 Compare December 31, 2025 18:22
codescene-delta-analysis[bot]

This comment was marked as outdated.

@darrenschwarz darrenschwarz marked this pull request as ready for review December 31, 2025 22:25
@darrenschwarz darrenschwarz marked this pull request as draft January 1, 2026 00:15
@iancooper

Copy link
Copy Markdown
Member

Likely back on Brighter stuff tomorrow

codescene-delta-analysis[bot]

This comment was marked as outdated.

@darrenschwarz darrenschwarz marked this pull request as ready for review January 1, 2026 17:53
  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.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@iancooper

Copy link
Copy Markdown
Member

@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
codescene-delta-analysis[bot]

This comment was marked as outdated.

  - 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
codescene-delta-analysis[bot]

This comment was marked as outdated.

  - 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
codescene-delta-analysis[bot]

This comment was marked as outdated.

@darrenschwarz

Copy link
Copy Markdown
Contributor Author

@darrenschwarz I am done commenting on this, for now, if you want to move it on.

All comments have been addressed, ready for review.

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@iancooper

Copy link
Copy Markdown
Member

@darrenschwarz Even I am getting confused as to whether the sign reversal was fixed now

@darrenschwarz

Copy link
Copy Markdown
Contributor Author

@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.

@iancooper

Copy link
Copy Markdown
Member

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 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.

@darrenschwarz

Copy link
Copy Markdown
Contributor Author

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 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. :)

@darrenschwarz

Copy link
Copy Markdown
Contributor Author

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 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.

@iancooper

Copy link
Copy Markdown
Member

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

@iancooper iancooper merged commit c8d27b2 into BrighterCommand:master Feb 1, 2026
52 of 57 checks passed
DevJonny pushed a commit to DevJonny/Brighter that referenced this pull request Feb 28, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] RabbitMQ Mutual TLS

2 participants