It Connects with None but Fails with Basic256Sha256: Debugging OPC UA Trust
Why a client browses fine with security off but drops the secure channel, and how to fix trust stores, endpoints and certificate names in a project.
The tag browse works on the bench. You flip the client from None to Basic256Sha256, and the secure channel dies before the first Read. Nothing on the wire looks wrong — ping is fine, the port is open, the server is up. This is the single most common OPC UA commissioning failure I run into, and it almost always comes down to trust, a certificate name, or an endpoint the client picked for itself.
Security is not IT paperwork bolted on after the fact. In the OPC UA security model (Part 2 of the spec), the secure channel sits underneath everything — tags, alarms, trends, writes. If the channel won't establish or keeps renewing and dropping, none of that data is trustworthy, no matter how clean your address space looks.
Stop letting the client auto-select the endpoint
Most servers advertise several endpoints from GetEndpoints: different URLs, security modes (None / Sign / SignAndEncrypt), and security policies. Left on default, a lot of drivers grab the first usable one, and "usable" often means the weakest. I've walked into commissioned systems running None in production because the client happily auto-selected it and nobody checked.
Pin the endpoint explicitly and write down what you pinned. The things that actually bite:
- Security policy. Use
Basic256Sha256as the floor.Basic128Rsa15and plainBasic256were deprecated and pulled from the current profiles (OPC UA Part 7) because they lean on SHA-1 — if your server still offers them, that's a red flag, not an option.Aes128Sha256RsaOaepandAes256Sha256RsaPssare the newer choices if both ends support them. - Security mode.
Signgets you authentication and integrity;SignAndEncryptadds confidentiality. Pick one on purpose. Don't ship with aNoneendpoint still enabled "for troubleshooting" — that's the one someone will quietly fall back to. - User token. Anonymous, username/password, or certificate. Browse can succeed anonymously while writes fail under a different identity, so test the identity operations will actually run as.
Record the selected endpoint URL and policy in the project notes. Otherwise the next engineer "fixes" a flaky connection by dropping to a weaker endpoint and never tells anyone.
Trust is mutual, and that trips people up
OPC UA certificate trust runs both directions: the client has to trust the server's certificate, and the server has to trust the client's. Most products dump the first unknown certificate into a rejected folder and refuse the connection until you move it. So the standard dance is:
- Install or generate the client's application instance certificate.
- Let the client connect once — it fails, but now the server has seen the cert and parked it in rejected.
- On the server, move that client cert from rejected to trusted.
- Grab the server's certificate and import it into the client's trusted store.
- Reload the OPC UA service if the product needs it (many do).
- Reconnect and confirm the live session is on the endpoint and policy you intended — not a fallback.
Folder names differ by vendor, but the pattern is universal. When a secure connection won't come up, check both trust stores before you touch scan rates or tag addresses. On a redundant pair this is the classic "works on primary only" bug: someone trusted the client cert on server A and forgot server B.
If you're running more than a handful of nodes, this manual shuffle stops scaling. That's what the Global Discovery Server and its push/pull certificate management (Part 12) exist for — centralized trust lists instead of per-node folder surgery. On a two-server SCADA job it's overkill; on a plant with fifty edge devices it's the difference between a rotation taking an afternoon or a week.
The certificate name has to match the URL you connect to
An OPC UA application instance certificate is not a generic TLS cert. The ApplicationUri in the certificate's SubjectAltName URI field has to match the URI the application announces, and the hostname in the URL you connect to has to be covered by the cert's subject or SAN (per RFC 5280 name matching). Break either and the channel is rejected even though the box pings fine.
The usual field version: a cert was generated for a developer's laptop hostname, and production connects by IP or by a different DNS alias. Now the SAN doesn't cover the endpoint and you get a host-name rejection. Before you leave site, confirm:
- The endpoint hostname the client uses is in the server cert's subject/SAN.
- DNS resolves the same name consistently from both SCADA servers — not just the one you tested from.
- If the server was renamed, its cert was regenerated afterward (a rename does not update the old cert).
- NAT, port forwarding, or a jump network isn't rewriting the endpoint into something the cert can't cover.
Ping proves reachability. It proves nothing about whether the security stack agrees.
Certificates expire, and nobody owns that
Default validity is often a year or two. If no one owns rotation, your first reminder is a production comms outage the morning a cert lapses — and if the server and client expire close together, both ends fail at once. Build an inventory: every server and client cert, its expiry, and which node it lives on. Set a maintenance reminder 30–60 days out. Keep the previous cert and trust-store state as a rollback, and test the swap on a redundant pair before you need it, because the trap is breaking trust on the standby while the primary still works.
After any cert change, run the short regression: browse, read, an allowed write, an alarm update, a historian value landing. On a validated or regulated system, the cert swap goes in the change-control package like any other baseline change — the cert is part of the communication baseline, not a loose file.
Reading the status codes
Secure-channel errors come back as OPC UA status codes (Part 4), and the text is terse. What each one usually means in the field:
Bad_SecurityChecksFailed— the catch-all. Cert not trusted, name mismatch, expired, or the policy isn't supported on one end. Start here.Bad_CertificateTimeInvalid— a clock is wrong or the cert isn't valid yet. Check NTP on both nodes before blaming the cert.Bad_CertificateHostNameInvalid— the endpoint hostname isn't in the cert's SAN.Bad_CertificateUriInvalid— the ApplicationUri and the cert's SAN URI don't match. Regenerating the cert against the right URI is the fix, not re-importing it.Bad_UserAccessDenied— the channel is up; the identity just lacks browse or write rights. This is an authorization problem, not a certificate one, and it's easy to misdiagnose.
A packet capture confirms the TCP socket opened and the OpenSecureChannel exchange happened, but it won't decode why the server rejected the cert. The server's own security log or diagnostics view will tell you far more, far faster.
Before you leave site
- Disable every leftover
Noneendpoint, or document exactly why one has to stay. - Confirm the live session is on the intended mode and policy, not an auto-selected fallback.
- Verify trust in both directions, on every redundant node.
- Log every cert's expiry into the maintenance system.
- Connect from every server, gateway, and standby that will run in production — reboot and reconnect to catch DNS surprises.
- Run one allowed write if the client will write in operation.
The rule I hold to: never sign off OPC UA comms on an insecure test endpoint. Prove the exact endpoint, certificate chain, trust state, and user identity production will run on. That's the connection the operators are betting on.