Security tests
Policy negotiation, certificate validation, authentication paths — the recipes for the security layer of your OPC UA client.
The recipes that exercise the policy, mode, certificate, and authentication layers.
Policy and mode tests
Discover all policies
GetEndpoints("opc.tcp://localhost:4843/UA/TestServer") # opcua-all-security
→ 11 EndpointDescriptions covering every (policy, mode) pair
For each endpoint:
verify EndpointDescription contains:
- endpointUrl
- securityPolicyUri
- securityMode
- userIdentityTokens (list of accepted token types)
- transportProfileUri
Connect with each policy on opcua-all-security
for each EndpointDescription ed:
if ed.securityMode == None:
connect_anonymous(ed)
→ Good
elif ed.userIdentityTokens contains Anonymous:
connect_anonymous(ed, with client cert)
→ Good
elif ed.userIdentityTokens contains UserName:
connect_username(ed, "admin", "admin123")
→ Good
The right test of "my client speaks every policy".
Negotiate to strongest available
A canonical client behaviour:
endpoints = GetEndpoints(...)
# Filter: drop deprecated, prefer SignAndEncrypt
candidates = [
ed for ed in endpoints
if ed.securityPolicy not in DEPRECATED
and ed.securityMode == SignAndEncrypt
]
# Pick by Strength (server-reported)
best = max(candidates, key=lambda e: e.securityLevel)
connect(best)
→ Good
Test against opcua-all-security — your client should pick
Aes256_Sha256_RsaPss + SignAndEncrypt or similar (strongest
non-deprecated).
Legacy policies
connect("opc.tcp://localhost:4847", Basic128Rsa15, SignAndEncrypt)
→ Good (server offers it)
→ Your client should ideally log a warning about deprecated policy
ECC negotiation
GetEndpoints("opc.tcp://localhost:4848")
→ 4 endpoints (ECC_nistP256 + ECC_nistP384) × (Sign + SignAndEncrypt)
connect with ECC_nistP256 + SignAndEncrypt
→ Good
Certificate validation tests
Trusted client cert
connect("opc.tcp://localhost:4842", Basic256Sha256, SignAndEncrypt,
client_cert = certs/client/cert.pem,
client_key = certs/client/key.pem)
→ Good
Self-signed (untrusted) cert
connect("opc.tcp://localhost:4842", ...,
client_cert = certs/self-signed/cert.pem,
client_key = certs/self-signed/key.pem)
→ Bad_CertificateUntrusted
Expired cert
connect("opc.tcp://localhost:4842", ...,
client_cert = certs/expired/cert.pem,
client_key = certs/expired/key.pem)
→ Bad_CertificateTimeInvalid
Auto-accept
connect("opc.tcp://localhost:4845", ...,
client_cert = certs/self-signed/cert.pem,
client_key = certs/self-signed/key.pem)
→ Good (auto-accept server accepts unknown certs)
Subsequent connections with the same self-signed cert also
succeed — the server moves it to its trusted/ dir on first
contact.
Application URI mismatch
connect with client_cert that has URI=urn:opcua:something-else
→ Bad_CertificateUriInvalid
The cert's subjectAltName URI must match the client's
ApplicationUri.
Authentication tests
Valid username
connect("opc.tcp://localhost:4841", ..., user="admin", pass="admin123")
→ Good, session created
Repeat for operator/operator123, viewer/viewer123, test/test.
Wrong password
connect(..., user="admin", pass="wrongpassword")
→ Bad_UserAccessDenied
Unknown user
connect(..., user="nonexistent", pass="x")
→ Bad_UserAccessDenied
Anonymous on userpass
connect("opc.tcp://localhost:4841", ..., identity=Anonymous)
→ Bad_IdentityTokenRejected
Note the distinction: invalid username/password combinations
return Bad_UserAccessDenied (the token type was accepted but
the credentials failed). Bad_IdentityTokenRejected is only
returned when the token type itself is not allowed on the
endpoint — Anonymous on a server with AllowAnonymous=false,
or Certificate on a server with AuthCertificate=false.
Role-based write rejection
# Connect as viewer
session = connect(..., user="viewer", pass="viewer123")
write(session, ns=1;s=TestServer/AccessControl/OperatorLevel/Setpoint, 60.0)
→ Bad_UserAccessDenied
Compare with operator (allowed):
session = connect(..., user="operator", pass="operator123")
write(session, ..., 60.0)
→ Good
Certificate auth happy path
connect("opc.tcp://localhost:4842", ...,
# Channel uses client cert
client_cert = certs/client/cert.pem,
# User identity is also a cert
identity = X509(certs/client/cert.pem, certs/client/key.pem))
→ Good
Certificate auth — wrong identity cert
connect("opc.tcp://localhost:4842", ...,
client_cert = certs/client/cert.pem,
identity = X509(certs/self-signed/cert.pem, ...))
→ Bad_CertificateUntrusted (or Bad_IdentityTokenRejected)
Trust-store tests
Server cert TOFU
A useful pattern for tests that don't want to pre-share certs:
1. Connect to opcua-userpass (4841) for the first time
2. Server presents its auto-generated cert
3. Your client captures fingerprint, stores it
4. Disconnect
5. Re-connect — client trusts the same fingerprint
If the cert changes between steps (server restarted with cert regen), the test should detect the change and fail.
CA cert validation
# Add certs/ca/ca-cert.pem to your client's trust store
# This validates the CA-signed certs but NOT the server's
# auto-generated cert (which is self-issued, not CA-signed)
connect(opcua-userpass, ...)
→ Bad_CertificateUntrusted (server cert not CA-signed)
Pair this with fingerprint pinning for a working setup.
Discovery tests
FindServers
client.connect("opc.tcp://localhost:4844") # discovery, no resource path
client.call_find_servers()
→ list containing (at most) the discovery server itself
None of the classic test servers in docker-compose.yml set
OPCUA_DISCOVERY_URL, and TestServerApp does not call
RegisterServer / RegisterServer2 against any discovery
endpoint. So FindServers against port 4844 will not return
the other suite servers — it returns only whatever the
discovery server has self-registered (effectively itself, or an
empty list, depending on the UA-.NETStandard stack version).
The discovery endpoint is provided primarily as a target for
testing the FindServers and GetEndpoints calls themselves,
not as a working server registry for the rest of the suite.
GetEndpoints on discovery server
GetEndpoints("opc.tcp://localhost:4844")
→ EndpointDescriptions for the discovery service itself
→ None/None + Basic256Sha256/SignAndEncrypt
Test matrix summary
| Test | Server | Expected |
|---|---|---|
| Connect anonymous | 4840 | Good |
| Connect anonymous on userpass | 4841 | Bad_IdentityTokenRejected |
| Valid creds | 4841 | Good |
| Wrong password | 4841 | Bad_UserAccessDenied |
| Trusted client cert | 4842 | Good |
| Self-signed cert | 4842 | Bad_CertificateUntrusted |
| Expired cert | 4842 | Bad_CertificateTimeInvalid |
| Any cert (auto-accept) | 4845 | Good |
| Sign-only channel | 4846 | Good (Sign mode) |
| Legacy policy | 4847 | Good (with warning) |
| ECC NIST | 4848 | Good (ECC negotiation) |
| ECC Brainpool | 4849 | Good (Brainpool negotiation) |
| FindServers | 4844 | Non-empty list |
This 13-test matrix is a strong "security layer works" battery.
Where to read next
- Security — the policy / mode / cert reference.
- Authentication — user accounts and cert identity.