From e6ec5a09486e1c835ca2fa8f79d2aadebd288612 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 11 Nov 2026 11:57:59 -0500 Subject: [PATCH] docs(opcuaclient): keep server host-name check opt-in; surface it in the Security panel (#837) Owner decision 2026-11-11: certificateValidation.validateServerHostName stays opt-in (unset = off). The AdminUI toggle moves from the "Subscriptions, limits & discovery" panel to the Security panel beside mode/policy, with help text on the NAT/IP caveat, IPv6 zone ids and the loose first-label match. Binding unchanged (tri-state, "Default (off)" leaves the key absent). OpcUaClient.md states the decision; drivers rule updated; source-scan test pins placement + caveats. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk --- .claude/rules/drivers.md | 3 +- docs/drivers/OpcUaClient.md | 12 +++++-- .../Drivers/Forms/OpcUaClientDriverForm.razor | 33 ++++++++++++++----- ...aClientDriverPageFormSerializationTests.cs | 28 ++++++++++++++++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.claude/rules/drivers.md b/.claude/rules/drivers.md index 8cfe9688a..8586e567e 100644 --- a/.claude/rules/drivers.md +++ b/.claude/rules/drivers.md @@ -293,7 +293,8 @@ host logger factory (wired in `DriverFactoryBootstrap`; the FOCAS and TwinCAT fa - **OpcUaClient server host-name check (#816):** the SDK checks the server certificate's host only with `checkDomain: true`; its 8-arg `DefaultSessionFactory.CreateAsync` hard-codes `false`, so driver, AdminUI browser and Client.Shared never checked before #816. Now `certificateValidation.validateServerHostName` (tri-state, unset = off = - unchanged; AdminUI keeps it unset) / CLI `--validate-host-name`. It checks the **configured** host (the rewritten + unchanged; AdminUI keeps it unset; **stays opt-in by owner decision #837** — the toggle lives in the form's Security +panel with the NAT/IPv6-zone/loose-match caveats) / CLI `--validate-host-name`. It checks the **configured** host (the rewritten `ConfiguredEndpoint.EndpointUrl`) and auto-accept accepts a mismatch. ⚠️ The SDK checks in TWO places — `DefaultSessionFactory.CreateChannelAsync` (GetEndpoints cert, BEFORE any security-mode test, so it fired on `None`) and `Session.OpenAsync` — so pass `checkDomain` only for a non-`None` endpoint (`OpcUaEndpointSelection.CheckDomainFor`). diff --git a/docs/drivers/OpcUaClient.md b/docs/drivers/OpcUaClient.md index 677493e6a..fd7014681 100644 --- a/docs/drivers/OpcUaClient.md +++ b/docs/drivers/OpcUaClient.md @@ -189,8 +189,16 @@ Consequences: - Test Connect (`OpcUaClientDriverProbe`) only calls GetEndpoints on an unsecured discovery channel — no session, so no certificate or host-name check. -**Policy: `certificateValidation.validateServerHostName`** (driver JSON; AdminUI driver form "Server host-name check"; -the browser reads the same JSON): +**Decision (#837, owner, 2026-11-11): the check stays OPT-IN — unset means off, and it will not become a default.** +Turning it on by default would refuse every NAT / port-published / connect-by-IP endpoint whose certificate does not +list that exact host, and every link-local IPv6 endpoint, on upgrade; and because the match is loose (first label) it +would not deliver the identity guarantee a default implies. Operators enable it per driver where trust is CA-issued +and certificate names are fully-qualified and site-unique. The AdminUI shows the toggle in the driver form's +**Security** panel (beside mode/policy) as "Server certificate host-name check", with help text covering the NAT/IP +caveat, IPv6 zone ids and the loose first-label match. + +**Policy: `certificateValidation.validateServerHostName`** (driver JSON; AdminUI driver form, Security panel, +"Server certificate host-name check"; the browser reads the same JSON): | Value | Meaning | |---|---| diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor index f52566321..1cbb7f2bd 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor @@ -111,6 +111,30 @@ } + @* #816 / #837: opt-in by owner decision (unset = off). Kept in the Security panel so it is + found next to the mode/policy it depends on; "Default (off)" leaves the key absent. *@ +
+ + + + + + +
+
+
+ Opt-in (off unless set). When validating, the server certificate must name the + endpoint host you configured; a miss refuses the connection + (BadCertificateHostNameInvalid). Applies to Sign / SignAndEncrypt only, never to + None, and auto-accept accepts a mismatch. Caveats: +
    +
  • NAT / port publish / connecting by IP: fails unless the certificate lists that exact host or IP — leave off.
  • +
  • IPv6 zone ids (fe80::1%eth0, link-local) never match — leave off.
  • +
  • Loose match: the first DNS label alone counts (plc1.site-b.corp passes a certificate for PLC1), so use fully-qualified, site-unique certificate names.
  • +
+ Most useful with CA-issued trust; adds little over a pinned self-signed certificate. +
+
@@ -195,15 +219,6 @@
Revoked certificates are always rejected.
-
- - - - - - -
Sign / SignAndEncrypt only (never on None): the server certificate must name the endpoint host you configured. Off for link-local IPv6.
-
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs index af598b20e..8774f94ba 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs @@ -208,6 +208,34 @@ public sealed class OpcUaClientDriverPageFormSerializationTests reparsed.CertificateValidation.EffectiveValidateServerHostName.ShouldBe(value ?? false); } + /// + /// #837: the host-name check stays opt-in (unset = off) and sits in the Security panel with help text naming + /// the NAT/IP, IPv6-zone and loose-first-label caveats. Source scan (no bUnit in this repo). + /// + [Fact] + public void The_host_name_check_is_an_opt_in_in_the_security_panel_with_its_caveats() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) + dir = dir.Parent; + dir.ShouldNotBeNull("could not locate the repository root"); + var razor = File.ReadAllText(Path.Combine(dir!.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.AdminUI", + "Components", "Shared", "Drivers", "Forms", "OpcUaClientDriverForm.razor")); + + razor.ShouldContain(""); + + var security = razor.IndexOf("
Security
", StringComparison.Ordinal); + var next = razor.IndexOf("
", security + 1, StringComparison.Ordinal); + security.ShouldBeGreaterThan(-1); + var panel = razor[security..next]; + panel.ShouldContain("_form.OpcUa.ValidateServerHostName"); + panel.ShouldContain("Opt-in"); + panel.ShouldContain("NAT"); + panel.ShouldContain("IPv6 zone ids"); + panel.ShouldContain("Loose match"); + razor.Split("_form.OpcUa.ValidateServerHostName").Length.ShouldBe(2, "the toggle must be rendered once"); + } + /// /// #648: stored DriverConfig JSON from before targetNamespaceKind/unsMappingTable were removed /// must still load into the form (the form's options use UnmappedMemberHandling.Skip) and the legacy