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
This commit is contained in:
@@ -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`).
|
||||
|
||||
@@ -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 |
|
||||
|---|---|
|
||||
|
||||
+24
-9
@@ -111,6 +111,30 @@
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
@* #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. *@
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="opcuaValidateHostName">Server certificate host-name check</label>
|
||||
<InputSelect id="opcuaValidateHostName" @bind-Value="_form.OpcUa.ValidateServerHostName" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
|
||||
<option value="">Default (off)</option>
|
||||
<option value="true">Validate configured host</option>
|
||||
<option value="false">Off (NAT / port publish)</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-text">
|
||||
<strong>Opt-in (off unless set).</strong> When validating, the server certificate must name the
|
||||
endpoint host <em>you configured</em>; a miss refuses the connection
|
||||
(<code>BadCertificateHostNameInvalid</code>). Applies to Sign / SignAndEncrypt only, never to
|
||||
<code>None</code>, and auto-accept accepts a mismatch. Caveats:
|
||||
<ul class="mb-0">
|
||||
<li><strong>NAT / port publish / connecting by IP:</strong> fails unless the certificate lists that exact host or IP — leave off.</li>
|
||||
<li><strong>IPv6 zone ids</strong> (<code>fe80::1%eth0</code>, link-local) never match — leave off.</li>
|
||||
<li><strong>Loose match:</strong> the first DNS label alone counts (<code>plc1.site-b.corp</code> passes a certificate for <code>PLC1</code>), so use fully-qualified, site-unique certificate names.</li>
|
||||
</ul>
|
||||
Most useful with CA-issued trust; adds little over a pinned self-signed certificate.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -195,15 +219,6 @@
|
||||
</div>
|
||||
<div class="form-text">Revoked certificates are always rejected.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Server host-name check</label>
|
||||
<InputSelect @bind-Value="_form.OpcUa.ValidateServerHostName" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
|
||||
<option value="">Default (off)</option>
|
||||
<option value="true">Validate configured host</option>
|
||||
<option value="false">Off (NAT / port publish)</option>
|
||||
</InputSelect>
|
||||
<div class="form-text">Sign / SignAndEncrypt only (never on None): the server certificate must name the endpoint host you configured. Off for link-local IPv6.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Issuer store / CRL directory</label>
|
||||
<InputText @bind-Value="_form.OpcUa.IssuerStorePath" @bind-Value:after="EmitAsync" class="form-control form-control-sm mono" />
|
||||
|
||||
+28
@@ -208,6 +208,34 @@ public sealed class OpcUaClientDriverPageFormSerializationTests
|
||||
reparsed.CertificateValidation.EffectiveValidateServerHostName.ShouldBe(value ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #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).
|
||||
/// </summary>
|
||||
[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("<option value=\"\">Default (off)</option>");
|
||||
|
||||
var security = razor.IndexOf("<div class=\"panel-head\">Security</div>", StringComparison.Ordinal);
|
||||
var next = razor.IndexOf("<div class=\"panel-head\">", 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #648: stored DriverConfig JSON from before <c>targetNamespaceKind</c>/<c>unsMappingTable</c> were removed
|
||||
/// must still load into the form (the form's options use <c>UnmappedMemberHandling.Skip</c>) and the legacy
|
||||
|
||||
Reference in New Issue
Block a user