Skip to content

tls: document Prefer mode plaintext behavior#1094

Merged
mtmk merged 6 commits into
mainfrom
tls-prefer-mitm-docs-and-test
Apr 3, 2026
Merged

tls: document Prefer mode plaintext behavior#1094
mtmk merged 6 commits into
mainfrom
tls-prefer-mitm-docs-and-test

Conversation

@mtmk

@mtmk mtmk commented Mar 19, 2026

Copy link
Copy Markdown
Member

Documents TLS Prefer/Auto mode behavior and adds guidance for TLS-terminating proxy setups (nats-io/nats-server#7927).

Unlike most other NATS clients (Go, Rust, Java, Python), the .NET client attempts a TLS upgrade when the server advertises tls_available without tls_required. This can cause connection failures behind a TLS-terminating proxy. The workaround is TlsMode.Disable.

We are not changing the default behavior to match other clients because existing configurations may rely on the automatic TLS upgrade. Switching to plaintext silently would be a security regression for those users.

  • XML doc remarks on NatsOpts.TlsOpts, NatsTlsOpts.Mode, TlsMode.Prefer, TlsMode.Auto
  • TLS proxy section in docfx security page
  • Real nats-server tests with tls {} + allow_non_tls
  • Go client cross-check via GoHarness
  • Fake-server tests for Prefer, Disable, Require, Auto modes

The TlsMode.Prefer XML docs said "if the Server supports TLS, then
use it" without mentioning that "supports TLS" comes from an
unauthenticated plaintext INFO message that a MITM can forge.

Add remarks to Prefer, Auto, and NatsTlsOpts.Mode explaining the
opportunistic nature and pointing users to tls:// or Require when
they actually need TLS.

Add a test that proves it: a fake TCP server strips the TLS flags
from INFO and the client happily sends CONNECT with credentials in
plaintext. This is by-design (every NATS client works this way),
but now it's documented and tested rather than implicit.
@synadia-claude-reviewer

synadia-claude-reviewer Bot commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mtmk's task in 3m 22s —— View job


PR Review: tls: document MITM downgrade risk in Prefer mode

  • Read changed files and gather context
  • Review NatsTlsOpts.cs documentation changes
  • Review TlsPreferMitmTest.cs test implementation
  • Check for correctness, security, and protocol issues
  • Post final review

Summary

Good PR. The documentation accurately describes the MITM downgrade risk, uses proper <see cref="..."/> cross-references, and points users to the correct mitigations. The test suite proves the by-design behavior. Two minor robustness issues in the tests worth addressing.

Documentation (NatsTlsOpts.cs)

No issues. The remarks are accurate, appropriately scoped to Prefer, Auto, and NatsTlsOpts.Mode, and consistently direct users to tls:// or TlsMode.Require as the secure alternative.

Tests (TlsPreferMitmTest.cs)

1. serverTask not awaited in Require_mode_throws_when_server_does_not_support_tls

After the ThrowAsync assertion, serverTask is cancelled and abandoned without await:

await act.Should().ThrowAsync<NatsException>();
cts.Cancel();
listener.Stop();
// serverTask is never awaited here

If serverTask faults (e.g., a socket error before the cancellation), the exception becomes unobserved, which xunit may surface as a test failure unrelated to the actual assertion. The fix is to await serverTask (swallowing OperationCanceledException) after cts.Cancel(). Fix this →

2. listener.Stop() not called on failure in Prefer_mode_sends_credentials_in_plaintext_when_info_has_no_tls_flags

If serverTask throws or nats.ConnectAsync() throws before await serverTask, listener.Stop() is skipped. The using var cts will be disposed, but the TcpListener remains open until GC. Not a test correctness issue, but wrapping in try/finally would be cleaner:

try
{
    await nats.ConnectAsync();
    await serverTask;
}
finally
{
    listener.Stop();
}

3. Encoding comment duplication

The // ISO 8859-1 (Latin-1): 1-byte encoding... comment is copy-pasted identically in both Require_mode_throws... and Prefer_mode_sends.... Minor nit; could be a shared constant or just accepted.

Protocol correctness

The INFO JSON in the MITM server includes "tls_required":false,"tls_available":false — both fields are explicitly set. This is correct: the client checks these flags to decide whether to upgrade. The PING/PONG handshake used to detect the completed CONNECT is consistent with how the real NATS protocol works.

@github-actions

github-actions Bot commented Mar 19, 2026

Copy link
Copy Markdown

mtmk added 2 commits March 26, 2026 13:32
Drop MITM/attacker framing from XML docs and test comments.
Prefer mode is simply opportunistic TLS; the connection may
stay plaintext when the server doesn't advertise TLS. Rename
test class and file to TlsPreferTest.
@mtmk mtmk changed the title tls: document MITM downgrade risk in Prefer mode tls: document Prefer mode plaintext behavior Mar 26, 2026
Document that Prefer/Auto mode attempts TLS upgrade when
the server advertises tls_available, which differs from
most other NATS clients. Add guidance to use TlsMode.Disable
behind a TLS-terminating proxy.

Add real nats-server tests for the tls_available scenario
and a Go client cross-check via GoHarness.
@mtmk
mtmk requested a review from philpennock March 27, 2026 11:01

@philpennock philpennock left a comment

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.

Did not review tests.

If it is not too late to rename Implicit, it would be good to do so, as "implicit" might be taken as "ambiently available without our seeing it", via things like stunnel or equivalent cryptographic privacy (tailscale etc). I'd name it OnConnect and make clear that it is a stronger form of Require. I'd also mention explicitly below the table that only Require and (OnConnect or Implicit) provide the full protection which TLS can offer.

Looks good as is, the feedback above is non-blocking.

@mtmk

mtmk commented Apr 3, 2026

Copy link
Copy Markdown
Member Author

Did not review tests.

If it is not too late to rename Implicit, it would be good to do so, as "implicit" might be taken as "ambiently available without our seeing it", via things like stunnel or equivalent cryptographic privacy (tailscale etc). I'd name it OnConnect and make clear that it is a stronger form of Require. I'd also mention explicitly below the table that only Require and (OnConnect or Implicit) provide the full protection which TLS can offer.

Looks good as is, the feedback above is non-blocking.

@philpennock thanks for the review! good point. We can deprecate Implicit. OnConncet would clash with .NET event naming convention though. Perhaps TlsFirst matching server? other options: Immediate, Preemptive, Eager. Any preference, suggestion?

Will also add the note about full TLS protection.

@philpennock

Copy link
Copy Markdown
Member

I retract my previous suggestion to avoid "Implicit". That's the RFC 8314 terminology so we should stick with it for consistency even if it is a little unfortunate.

mtmk added 2 commits April 3, 2026 15:10
Add remarks to TlsMode enum, Mode property, and security docs
clarifying that other modes may fall back to plaintext.
The GoHarness test downloads Go 1.25.x at runtime when the
runner has an older version. Pin go-version to stable so the
runner already has the latest Go, and bump the test timeout
from 60s to 180s for slow CI environments.
@mtmk

mtmk commented Apr 3, 2026

Copy link
Copy Markdown
Member Author

test failures for 'Check_JetStream_cluster_related_fields' are not related to this PR. We can investigate after merge to main.

@mtmk
mtmk merged commit ac82e28 into main Apr 3, 2026
22 of 24 checks passed
@mtmk
mtmk deleted the tls-prefer-mitm-docs-and-test branch April 3, 2026 15:48
This was referenced Apr 16, 2026
@mtmk mtmk mentioned this pull request May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants