diff --git a/src/NATS.Server/WebSocket/WebSocketTlsConfig.cs b/src/NATS.Server/WebSocket/WebSocketTlsConfig.cs
new file mode 100644
index 0000000..c7404aa
--- /dev/null
+++ b/src/NATS.Server/WebSocket/WebSocketTlsConfig.cs
@@ -0,0 +1,78 @@
+namespace NATS.Server.WebSocket;
+
+///
+/// Separate TLS configuration for the WebSocket listener,
+/// allowing different certificates than the main NATS listener.
+/// Go reference: server/websocket.go — wsTLSConfig.
+///
+public sealed class WebSocketTlsConfig
+{
+ public string? CertFile { get; set; }
+ public string? KeyFile { get; set; }
+ public string? CaFile { get; set; }
+ public bool RequireClientCert { get; set; }
+ public bool InsecureSkipVerify { get; set; }
+
+ /// Returns true when a certificate file has been specified.
+ public bool IsConfigured => !string.IsNullOrWhiteSpace(CertFile);
+
+ ///
+ /// Validates the TLS configuration.
+ /// An empty configuration (no cert, no key) is valid and means no TLS.
+ /// When either cert or key is specified, both must be provided.
+ ///
+ public WebSocketTlsValidationResult Validate()
+ {
+ var errors = new List();
+
+ bool hasCert = !string.IsNullOrWhiteSpace(CertFile);
+ bool hasKey = !string.IsNullOrWhiteSpace(KeyFile);
+
+ if (hasKey && !hasCert)
+ errors.Add("CertFile must be specified when KeyFile is set.");
+
+ if (hasCert && !hasKey)
+ errors.Add("KeyFile must be specified when CertFile is set.");
+
+ return new WebSocketTlsValidationResult(errors.Count == 0, errors);
+ }
+
+ ///
+ /// Returns true when this configuration differs from .
+ /// Used for hot-reload diff detection.
+ ///
+ public bool HasChangedFrom(WebSocketTlsConfig? other)
+ {
+ if (other is null)
+ return true;
+
+ return CertFile != other.CertFile
+ || KeyFile != other.KeyFile
+ || CaFile != other.CaFile
+ || RequireClientCert != other.RequireClientCert
+ || InsecureSkipVerify != other.InsecureSkipVerify;
+ }
+
+ ///
+ /// Creates a from the WebSocket-specific TLS fields
+ /// in , or returns null if no WebSocket TLS is configured.
+ ///
+ public static WebSocketTlsConfig? FromOptions(NatsOptions options)
+ {
+ var ws = options.WebSocket;
+
+ if (string.IsNullOrWhiteSpace(ws.TlsCert) && string.IsNullOrWhiteSpace(ws.TlsKey))
+ return null;
+
+ return new WebSocketTlsConfig
+ {
+ CertFile = ws.TlsCert,
+ KeyFile = ws.TlsKey,
+ };
+ }
+}
+
+/// Result of .
+public sealed record WebSocketTlsValidationResult(
+ bool IsValid,
+ IReadOnlyList Errors);
diff --git a/tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs b/tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs
new file mode 100644
index 0000000..2c99e54
--- /dev/null
+++ b/tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs
@@ -0,0 +1,119 @@
+// Go reference: server/websocket.go — wsTLSConfig and related TLS handling.
+
+using NATS.Server.WebSocket;
+using Shouldly;
+
+namespace NATS.Server.Tests.WebSocket;
+
+///
+/// Tests for — WebSocket-specific TLS configuration
+/// with separate cert/key from the main NATS listener (Gap 15.1).
+///
+public class WebSocketTlsTests
+{
+ // ── IsConfigured ──────────────────────────────────────────────────────────
+
+ [Fact]
+ public void IsConfigured_WithCert_ReturnsTrue()
+ {
+ var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
+ cfg.IsConfigured.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IsConfigured_WithoutCert_ReturnsFalse()
+ {
+ var cfg = new WebSocketTlsConfig();
+ cfg.IsConfigured.ShouldBeFalse();
+ }
+
+ // ── Validate ──────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void Validate_ValidConfig_NoErrors()
+ {
+ var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
+ var result = cfg.Validate();
+ result.IsValid.ShouldBeTrue();
+ result.Errors.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Validate_KeyWithoutCert_HasError()
+ {
+ var cfg = new WebSocketTlsConfig { KeyFile = "server-key.pem" };
+ var result = cfg.Validate();
+ result.IsValid.ShouldBeFalse();
+ result.Errors.ShouldNotBeEmpty();
+ }
+
+ [Fact]
+ public void Validate_CertWithoutKey_HasError()
+ {
+ var cfg = new WebSocketTlsConfig { CertFile = "server.pem" };
+ var result = cfg.Validate();
+ result.IsValid.ShouldBeFalse();
+ result.Errors.ShouldNotBeEmpty();
+ }
+
+ [Fact]
+ public void Validate_EmptyConfig_Valid()
+ {
+ // An empty configuration means "no TLS" — that is a valid state.
+ var cfg = new WebSocketTlsConfig();
+ var result = cfg.Validate();
+ result.IsValid.ShouldBeTrue();
+ result.Errors.ShouldBeEmpty();
+ }
+
+ // ── HasChangedFrom ────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HasChangedFrom_SameConfig_ReturnsFalse()
+ {
+ var a = new WebSocketTlsConfig
+ {
+ CertFile = "server.pem",
+ KeyFile = "server-key.pem",
+ CaFile = "ca.pem",
+ RequireClientCert = true,
+ InsecureSkipVerify = false,
+ };
+
+ var b = new WebSocketTlsConfig
+ {
+ CertFile = "server.pem",
+ KeyFile = "server-key.pem",
+ CaFile = "ca.pem",
+ RequireClientCert = true,
+ InsecureSkipVerify = false,
+ };
+
+ a.HasChangedFrom(b).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void HasChangedFrom_DifferentCert_ReturnsTrue()
+ {
+ var a = new WebSocketTlsConfig { CertFile = "old.pem", KeyFile = "old-key.pem" };
+ var b = new WebSocketTlsConfig { CertFile = "new.pem", KeyFile = "new-key.pem" };
+
+ a.HasChangedFrom(b).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void HasChangedFrom_NullOther_ReturnsTrue()
+ {
+ var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
+ cfg.HasChangedFrom(null).ShouldBeTrue();
+ }
+
+ // ── Defaults ──────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void RequireClientCert_Default_False()
+ {
+ var cfg = new WebSocketTlsConfig();
+ cfg.RequireClientCert.ShouldBeFalse();
+ }
+}