Skip to content

Server Security

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

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

The thermostat from Your first server listens on loopback with SecurityPolicy None and anonymous access. That is fine on one machine. On a network, anyone who can reach the port could read Temperature, change Setpoint, and call AdjustSetpoint, and nothing on the wire would be signed or encrypted.

This page gives the thermostat an application identity, a trust list for client applications, a secured endpoint, and a user check, then covers certificate rotation and resource ownership. It builds on the endpoint settings from Server configuration. Deciding what each authenticated user may do comes next, in Access control.

Two identities and a trust list

An OPC UA connection authenticates twice. When a client opens a SecureChannel, the two applications exchange application certificates, and each validates the other's against its own trust list. Then the client creates a Session and presents a user token, such as a username and password, for the server's identity validator to check. The application certificate authenticates the program, not the person or service account using it, so user credentials and X.509 user tokens are separate identities.

Milo keeps the server's side of the first check in a CertificateGroup, which combines certificate types and the server's own identities (private key and certificate chain) with a trust list, a validator, and a quarantine for rejected certificates. DefaultCertificateGroup is backed by a certificate store. A CertificateManager holds the groups, and each secured endpoint selects a compatible identity from a group when the server resolves its endpoints.

Establish the application identity

Clients trust the server by its application certificate, so keep the private key across ordinary restarts and protect it in storage. The certificate's subject alternative names (SANs) must include the persistent application URI and the DNS names and IP addresses the server advertises.

The thermostat keeps its key in a PKCS#12 keystore and creates a self-signed certificate the first time it starts. In this code, securityDir is an existing, application-owned persistent directory, keyStorePassword is a Supplier<char[]> from your secret handling, and hostname is the name clients use to reach the server.

var certificateStore =
    KeyStoreCertificateStore.createAndInitialize(
        new KeyStoreCertificateStore.Settings(
            securityDir.resolve("thermostat.pfx"),
            keyStorePassword,
            alias -> keyStorePassword.get()));

var certificateFactory =
    new AbstractCertificateFactory() {
      @Override
      protected X509Certificate[] createRsaSha256CertificateChain(KeyPair keyPair)
          throws Exception {
        X509Certificate certificate =
            SelfSignedCertificateBuilder.forRsaApplicationCertificate(keyPair)
                .setCommonName("Thermostat")
                .setApplicationUri(applicationUri)
                .addDnsName(hostname)
                .build();

        return new X509Certificate[] {certificate};
      }
    };

The factory runs only when the group has no certificate in its store. setApplicationUri and addDnsName fill the SANs that clients check, so they must match the server's configuration. Choose file permissions and secret handling that fit your deployment.

If a CA issues your certificates instead, skip the factory and its createMissingCertificates call, and install the issued key pair and chain with group.updateCertificate(...). DefaultCertificateGroup.forIdentity(...) wraps an existing key pair and chain in a group with the trust components you supply.

The four-argument DefaultCertificateGroup constructor used below supports only RsaSha256ApplicationCertificateType. To use ECC, pass the intended certificate-type NodeIds through its five-argument constructor and provision matching identities, for example by overriding the matching ECC chain method of AbstractCertificateFactory. Adding an ECC endpoint does not add certificate types or generate missing material.

Trust and rejection

The trust list decides which client applications may open a SecureChannel. It keeps trusted peer and CA certificates, issuer certificates, and CRLs separate, and issuer material helps build chains but is not automatically a trust anchor. The thermostat keeps its trust list in directories next to the keystore, with a file-based quarantine for rejected certificates.

Path pkiDir = securityDir.resolve("pki");
var trustListManager = FileBasedTrustListManager.createAndInitialize(pkiDir);

var quarantine =
    FileBasedCertificateQuarantine.create(pkiDir.resolve("rejected").resolve("certs"));

var validator = new DefaultServerCertificateValidator(trustListManager, quarantine);

createAndInitialize creates trusted/certs, trusted/crl, issuer/certs, and issuer/crl under pkiDir, then starts a background thread that reloads a directory when its files change. For a test, MemoryTrustListManager and MemoryCertificateQuarantine from the same package hold this state in memory and persist nothing.

With an empty trust list, an unknown application certificate fails validation with Bad_SecurityChecksFailed and goes into quarantine. So the first connection from a new client is rejected, and its certificate appears in pki/rejected/certs. Treat rejected certificates as evidence to inspect, not an auto-approval queue. Verify the peer's identity through an independent channel, for example by comparing the certificate thumbprint with the client's administrator, and only then copy the certificate into pki/trusted/certs. Removing it later makes validation fail again.

The two-argument validator constructor uses ValidationCheck.NO_OPTIONAL_CHECKS, which still includes the application URI check when a URI is provided. Choose additional checks deliberately with the three-argument constructor. Validation always fails when an available CRL lists a certificate in the path as revoked, and REVOCATION_LISTS also requires usable revocation information for CA-issued paths. Supply and refresh CRLs according to your PKI, because missing revocation data is not proof that a certificate is unrevoked.

Quarantine is per group. The manager's rejected list is the de-duplicated union of all group quarantines.

Custom trust lists and validators

If you subclass FileBasedTrustListManager or MemoryTrustListManager, put custom trust-list reads in a getSnapshot() override. Default certificate validators read the snapshot directly, so overrides of individual getters do not affect validation. Derive the snapshot from super.getSnapshot() and keep the getters consistent with it. See trust-list migration.

When subclassing DefaultServerCertificateValidator, put custom checks in its four-argument validation override and preserve the profile when calling super. SDK calls bypass an override of only the older three-argument method. See the validator migration note.

Combine identity and trust in a group

The group ties the keystore and the trust components together, the factory fills in missing certificate types, and the certificate manager hands the group to the server.

var group =
    new DefaultCertificateGroup(trustListManager, certificateStore, quarantine, validator);
certificateFactory.createMissingCertificates(group);

var certificateManager = new DefaultCertificateManager(group);

createMissingCertificates(group) installs material only for supported types the store is missing, so later starts reuse the stored identity. It never replaces material it cannot read. The read failure propagates instead.

new DefaultCertificateManager(group) registers the group as the DefaultApplicationGroup. Register any additional groups under the NodeIds of their CertificateGroupType instances, such as NodeIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup, not the CertificateGroupType type-definition NodeId.

The classes in these fragments are in org.eclipse.milo.opcua.stack.core.security, except SelfSignedCertificateBuilder in org.eclipse.milo.opcua.stack.core.util.

Offer secured endpoints

The thermostat's network endpoint replaces SecurityPolicy None with Basic256Sha256 and SignAndEncrypt, so every message is signed and encrypted, and it accepts only username tokens. It binds all IPv4 interfaces and advertises hostname, because clients connect to the advertised address and cannot use an all-interface bind address.

EndpointConfig endpoint =
    EndpointConfig.newBuilder()
        .setBindAddress("0.0.0.0")
        .setHostname(hostname)
        .setBindPort(12686)
        .setPath("/wiki")
        .setSecurityPolicy(SecurityPolicy.Basic256Sha256)
        .setSecurityMode(MessageSecurityMode.SignAndEncrypt)
        .addTokenPolicy(OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME)
        .build();

A secure endpoint with neither a fixed certificate (setCertificate(...)) nor a selection request selects a compatible identity from the DefaultApplicationGroup, here the group registered above, so this endpoint follows certificate rotation. To select from a different registered group or request a certificate type, call EndpointConfig.Builder.setEndpointCertificateConfig(EndpointCertificateConfig).

The security policy determines which identities are compatible. Choose a policy supported by both peers and by the JVM providers, certificate type, and installed key. Basic256Sha256 and the other legacy RSA policies, including Aes256_Sha256_RsaPss, work with built-in Java 17 providers. Enhanced ECC and RSA-DH policies have additional requirements. Supported ECC URIs end in _AesGcm or _ChaChaPoly, and suffix-less ECC URIs are not aliases. See Security policies for the exact supported families and compatibility limits.

Removing the None endpoint does not hide the server from discovery, because an unsecured SecureChannel on that path can still call the discovery services, such as GetEndpoints. See Discovery.

Authenticate users

Set the advertised user-token policies and the matching identity validator together, because clients choose from the advertised policies and the server accepts only tokens its validator understands. The thermostat checks usernames and passwords with UsernameIdentityValidator from org.eclipse.milo.opcua.sdk.server.identity. Here checkPassword is a BiPredicate<String, String> that checks a username and password against your credential store, and builder is the server's OpcUaServerConfigBuilder.

var identityValidator =
    new UsernameIdentityValidator(
        challenge -> checkPassword.test(challenge.getUsername(), challenge.getPassword()));

builder
    .setApplicationUri(applicationUri)
    .setCertificateManager(certificateManager)
    .setEndpoints(Set.of(endpoint))
    .setIdentityValidator(identityValidator);

When the predicate returns true, the Session gets a username Identity, which Access control maps to roles. The application URI passed to the builder must match the URI in the certificate's SANs.

OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME explicitly selects Basic256Sha256 to protect the password, which suits this endpoint but not an ECC channel. Protect username tokens with advertised policy and channel requirements, and do not expose real credentials on a None endpoint. To accept certificates as user identities, use X509IdentityValidator with a real certificate identity policy, and use CompositeValidator to accept more than one mechanism.

Start and verify the secured server

Start the server as described in Server configuration. Startup succeeds even when a secure endpoint cannot be resolved, so compare getBoundEndpoints() with the listening endpoints you require and treat a missing secured endpoint as a failed startup.

Then establish trust on both peers. The first connection from a new client fails until you approve its certificate, and the client must likewise trust the thermostat's self-signed certificate (see Client security). A successful discovery response does not prove secure Session establishment, so verify by connecting with the intended secure policy, opening a Session with a real user, and performing an operation.

Rotate certificates

Because the thermostat's endpoint uses managed selection without a fixed certificate, you renew it by installing the renewed identity in its group and then calling server.resetEndpointDescriptionCache(), so later advertisements resolve the current material.

An endpoint built with setCertificate(X509Certificate) and no selection request keeps advertising its fixed certificate after the reset. If the certificate manager no longer holds the old identity, new SecureChannels naming its thumbprint fail with Bad_CertificateInvalid. Use managed selection without setCertificate(...) for endpoints you intend to rotate.

Updating a store does not decide how your deployment retires established channels or Sessions, so test client rediscovery, trust updates, and reconnect. Recalculating advertisements does not rebind sockets. An endpoint newly satisfiable after provisioning may be advertised without a new listening socket, and an endpoint bound at startup stays bound even if it later becomes unresolvable. Changing the bound set requires a fresh server lifecycle.

When things go wrong

Before trust is established, the server masks detailed path errors as Bad_SecurityChecksFailed, and it masks revoked and revocation-unavailable errors the same way even on a trusted path. The validator debug logs hold the underlying cause. Other failures on a trusted path keep their specific status. A client's validation of the server certificate is separate and may report specific statuses to that client application.

Symptom Likely cause What to do
A secure endpoint is missing from discovery and getBoundEndpoints(), and startup logged a warning Its security provider is unusable, managed identity selection failed, or a fixed certificate is incompatible with an enhanced policy Match the certificate type to the policy and check the provider. See Security policies.
The client's SecureChannel fails with Bad_SecurityChecksFailed The client certificate is not trusted yet, or the server masked a path or revocation error Inspect the quarantine and the validator debug logs. Trust the certificate only after independent verification, and supply CRLs.
Bad_CertificateUriInvalid on a trusted path The client's application URI does not match its certificate Fix the client's certificate or its configured application URI.
Requests other than discovery fail with Bad_SecurityPolicyRejected The client opened an unsecured channel on a path with no None endpoint Connect through a secured endpoint.
Session activation fails with Bad_IdentityTokenInvalid The validator rejected the username or password. This encrypted Basic256Sha256 path reports that status, and other username-token paths can return Bad_UserAccessDenied. Check the credentials and the validator.

Known limitations

At this baseline, Milo contains the generated ServerConfigurationType and certificate-management Methods but no complete production Push management handlers, so their presence in the address space does not make UpdateCertificate, CreateSigningRequest, TrustList writes, or ApplyChanges operational. An application implementing Push must supply authorization, validation, persistence, transactional application, channel and Session consequences, and auditing. For a GDS Pull workflow, use the separate GDS client integration and keep scheduling and installation under application control.

Resource ownership

Your application owns the groups, certificate stores, trust-list managers, and quarantines it creates, along with their file watchers. For the thermostat, close the KeyStoreCertificateStore and the FileBasedTrustListManager after the server shuts down. Replacing or removing a group does not close its resources. Stop server activity and concurrent lookups before closing owned components, and close only resources your application created.

Coordinate writes from multiple store instances or processes, because a keystore's atomic file replacement does not provide a cross-process lock. Persisted trust-list writes can fail, and later directory reloads reconcile actual files. Monitor failures and verify the installed state.

Examples and reference

Next steps

The thermostat now knows which application and which user is behind each Session, but every authenticated user can still change Setpoint. To decide what each user may do, continue with Access control.

Related: Server SDK · Security policies · Client security · GDS client · Deployment

Clone this wiki locally