Skip to content

Client Security

Kevin Herron edited this page Oct 12, 2026 · 10 revisions

Milo 1.2.0-RC1, Java 17. Source baseline 69cb25087804.

Every operation so far, from reading Temperature to writing through node wrappers, ran on the tutorial server's None endpoint with an anonymous user. That is fine on loopback, but on a real network anyone who can see or alter the traffic can read the setpoint, change what you write, or pretend to be the server. This page gives the client an application identity, makes it validate the server's certificate, and helps you choose an endpoint and user token that protect the traffic. The tutorial server has no security, so the examples assume a server that offers Basic256Sha256 with SignAndEncrypt and whose certificate you obtained through your own provisioning process.

Application identity, user identity, and server trust

A secured connection involves three separate decisions. The application certificate identifies the client program. The client presents it during SecureChannel and Session establishment, and the matching private key proves possession. The server's certificate identifies the remote application, and the client's certificate validator decides whether to trust it. The user identity, which an identity provider supplies during Session activation, says who is using the program. These settings are independent of each other, and a server can apply separate trust and authorization policies to the client application and to its user.

The configured application URI must match the URI in the application's certificate. If you omit an explicit URI, Milo can derive it from the selected identity.

X509IdentityProvider belongs to the user side. It supplies a certificate user token during Session activation, which identifies the user and proves possession of that user's private key. It does not configure the client's application certificate, make the server trust the application, or establish the client's trust in the server.

Connect to a secured endpoint

setCertificateIdentity(keyPair, certificateChain) is the simple way to give the client one application identity. The chain is leaf-first, followed by its issuers. The builder checks that the key matches the certificate and infers the certificate type, and an identity that is incompatible with the endpoint's policy cannot be selected just because it contains an RSA key.

Checking servers is a separate setting. The client validates server certificates only when it has a validator, either from a certificate group or set with setCertificateValidator(...), so an application key and certificate alone are not enough.

The following method trusts one server certificate, trustedServer, that you obtained and approved through an independent provisioning process. It selects Basic256Sha256 with SignAndEncrypt, enables all optional validator checks, and enables Session endpoint validation. It returns an unconnected client, which you connect and disconnect with the lifecycle in Connecting.

static OpcUaClient secureClient(
    String discoveryUrl,
    KeyPair keyPair,
    X509Certificate[] certificateChain,
    X509Certificate trustedServer)
    throws UaException {
  var trust = new MemoryTrustListManager();
  trust.addTrustedCertificate(trustedServer);
  var quarantine = new MemoryCertificateQuarantine();
  var validator =
      new DefaultClientCertificateValidator(
          trust, ValidationCheck.ALL_OPTIONAL_CHECKS, quarantine);
  return OpcUaClient.create(
      discoveryUrl,
      endpoints ->
          endpoints.stream()
              .filter(
                  e -> SecurityPolicy.Basic256Sha256.getUri().equals(e.getSecurityPolicyUri()))
              .filter(e -> e.getSecurityMode() == MessageSecurityMode.SignAndEncrypt)
              .findFirst(),
      transport -> {},
      config ->
          config
              .setCertificateIdentity(keyPair, certificateChain)
              .setCertificateValidator(validator)
              .setSessionEndpointValidationEnabled(true)
              .setIdentityProvider(new AnonymousProvider()));
}

The validator checks every server certificate against a trust list that holds only trustedServer, and it quarantines any chain that does not build a trusted path to it. Session endpoint validation compares the endpoints in the CreateSession response with the list from discovery. Discovery is unauthenticated, so never fill trustedServer by automatically trusting whatever discovery returns. The client still authenticates anonymously. To authenticate a user as well, choose an identity provider as described in Choose endpoint and user-token policies.

Use setCertificateGroup(group) instead of setCertificateIdentity when the application maintains grouped identities and trust material, including material received through a GDS. The two settings replace each other. A group supplies its own validator unless setCertificateValidator(...) explicitly overrides it.

Decide which servers to trust

MemoryTrustListManager and MemoryCertificateQuarantine suit an example like this one. In production, persist approved trust and rejected-certificate records when operational requirements call for it.

A certificate chain is trusted only when its path includes a certificate from the trusted list. Issuer certificates help build the chain, but they do not make it trusted. Approve either the server application's certificate or an appropriate issuing trust anchor, following your provisioning policy.

For this Basic256Sha256 example, ALL_OPTIONAL_CHECKS requires suitable KeyUsage and ExtendedKeyUsage extensions, and usable CRLs for CA-issued certificates. A directly trusted self-signed application certificate has no issuer CRL requirement. A less strict configuration can permit unknown revocation status, but Milo still enforces a known revocation.

The two-argument DefaultClientCertificateValidator constructor enables application-URI checking but omits HOSTNAME and the other optional checks. It logs a warning on a hostname mismatch and does not reject the certificate for that alone. To enforce the hostname check, enable ValidationCheck.HOSTNAME, which ALL_OPTIONAL_CHECKS includes. During CreateSession the check uses the selected endpoint URL, so a host override changes which hostname is checked.

Discovery and endpoint certificate refresh never authorize a newly advertised certificate. Recovery and limits describes how rotation works.

Choose endpoint and user-token policies

Filter endpoints explicitly by transport, security policy URI, and message security mode, as the example does. Select a policy that both Milo's configured crypto providers and the peer support. A generated enum value or an advertised endpoint is not evidence that a particular vendor combination interoperates. Security policies lists the supported policies and their provider requirements.

An endpoint's user-token policy can specify its own security policy or inherit the endpoint's. Make sure the identity provider you choose has a compatible token policy and the certificate and key material it needs.

Username tokens need the most care. By default, UsernameProvider selects the first usable username policy the endpoint advertises. An explicit None policy is usable, and an empty policy URI inherits None on a None endpoint. Milo does not prefer token encryption or warn when this selection leaves the password unencrypted inside the token. On a None or Sign-only channel, the password then crosses the wire in cleartext, while SignAndEncrypt protects it with channel encryption.

To avoid that, select SignAndEncrypt, or supply a deterministic policyChooser. The chooser should select a compatible encrypted token policy and throw an unchecked exception if none meets your requirements. It receives the usable policies, or the full advertised username list when none is usable. It gets no endpoint argument, so classify inherited policies using the selected endpoint's policy.

Either way, validate the server certificate. On a secured OPC TCP channel, configure the client's certificate validator. On a None endpoint, the validator passed to UsernameProvider is the only check of the certificate that encrypts the password. Both two-argument constructors, which take a String password or a Supplier<byte[]>, skip that check. When a None endpoint with encrypted tokens is unavoidable, use the matching three-argument overload, or the four-argument overload with a chooser, and pass a real validator.

Anonymous authentication provides no user credential, even on an encrypted channel. X.509 user-token policies require proof of private-key possession, so a certificate-token policy that resolves to None is not usable for that proof.

Customize trust lists, validators, and identity providers

If you subclass FileBasedTrustListManager or MemoryTrustListManager, put custom trust-list reads in a getSnapshot() override, because the default validators read the snapshot and ignore overridden getters. Server security and the trust-list migration note explain the rule.

When you subclass DefaultClientCertificateValidator, override its four-argument validation method for custom checks, and call the four-argument super method with the same profile. SDK calls bypass an override of only the older three-argument method. The validator migration note explains the delegation. Subclasses of UsernameProvider, X509IdentityProvider, or CompositeProvider must likewise use the context-based identity method.

When things go wrong

The client validator quarantines chains that fail trusted-path building, but it does not quarantine every validation failure.

Failure Status Quarantined?
PKIX cannot build a trusted path, including an empty trust list Can report Bad_SecurityChecksFailed Yes
A built path contains no trusted certificate Bad_CertificateUntrusted Yes
URI mismatch with APPLICATION_URI enabled Bad_CertificateUriInvalid No
Hostname mismatch with HOSTNAME enabled Bad_CertificateHostNameInvalid No

Treat the quarantine as diagnostic evidence, never as automatic approval. Before you change trust, check the failure status, certificate validity, hostnames, application URI, key usage, chain, and policy compatibility. Never replace validation with an insecure validator to make a production connection failure go away.

Ownership

The application owns private-key protection, certificate enrollment and renewal, trust approval, persistence, and the lifecycle of its stores. Disconnect clients before closing stores or executors you supplied, and give resources shared by several clients a shared owner. The in-memory objects in this example hold no file watchers to close. File-backed production implementations can own watchers and must be closed according to their contracts.

Examples and reference

Next steps

Your client can now connect to a secured server and knows which servers it trusts. To keep it working through network faults, server restarts, certificate rotation, and server operation limits, continue with Recovery and limits.

Related: Client SDK · Security policies · Connecting · GDS client

Clone this wiki locally