← Articles
OPC UA/9 min read/ views

In OPC UA, a Trusted Certificate Is Not Permission to Write

How OPC UA separates certificate trust, user identity and Part 18 role mapping — and commissioning writes so the wrong session can't move a setpoint.

OPC UASCADANetworkingTroubleshootingProject Notes

An integrator once handed me a "secured" OPC UA server: Basic256Sha256 policy, mutual certificate trust, encryption on every message. It also let the historian collector account write to the mixer speed setpoint, because nobody had configured user permissions past "make it connect." The channel was encrypted end to end and completely wrong about who was allowed to do what.

That gap is the whole problem. In OPC UA there are three different questions hiding inside the word "security," and they get answered in three different places in the stack:

  • Which application may open a channel? — the application instance certificate (Part 4 OpenSecureChannel, Part 12 for GDS-managed trust).
  • Who is this session? — the user identity token passed to ActivateSession (anonymous, username/password, X.509, or issued token).
  • What may this session do to that node? — role mapping and the RolePermissions / UserRolePermissions attributes (Part 18, Role-Based Security).

A client can pass the first two and still have no business writing a tag. Get these confused during commissioning and you spend a day chasing a "certificate error" that was actually a role that was never granted.

The identity token is chosen at ActivateSession, so pick it deliberately

The user identity is separate from the certificate and gets presented when the session activates. OPC UA defines four token types, and the choice is an operations decision more than a security-strength decision:

Identity tokenWhere it fitsThe thing that bites you later
AnonymousRead-only diagnostics, an isolated demoLeft enabled after FAT because a test client needed it
Username/passwordSCADA service account, engineering loginRotation, and everyone sharing one opcuser
X.509 user certificateStrong user-to-session bindingA second certificate lifecycle to track and revoke
Issued token (JWT/OAuth2)Federated identity providerClock skew, token TTL, and whether your server even supports it

For plant SCADA I default to a named service account per integration path — separate identities for the HMI runtime, the historian collector, the MES gateway, and the engineering workstation — over one shared login. It costs a few minutes at setup and it's the difference between an audit log that reads HistorianCollector and one that reads opcuser for everything, at which point the audit trail tells you nothing.

Map roles to operations, and don't trust the folder

The tempting shortcut is to grant access by address-space folder: "Operators can write Area1." Area1 contains status tags, command tags, recipe values, bypass bits, and diagnostic controls, and those carry very different risk. Folder-level grants smear over that distinction.

Part 18 gives you well-known roles to hang permissions on — Anonymous, AuthenticatedUser, Observer, Operator, Engineer, Supervisor, ConfigureAdmin, SecurityAdmin — and each node carries RolePermissions (and per-session UserRolePermissions) that say what each role may Browse, Read, Write, or Call. That's the model to design against, even if you end up expressing it through a vendor's proprietary role UI, because plenty of servers still don't implement the Part 18 node model fully. What matters is a matrix of operations, not folders:

RoleBrowseReadSetpoint writeCommand writeAck alarmsMethod call
HistorianCollectorYesYesNoNoNoNo
HmiOperatorYesYesLimitedLimitedYesSelected
MaintenanceYesYesLimitedLimitedYesDiagnostics only
MesGatewayYesYesRecipe/download onlyNo direct jogNoSelected
EngineeringYesYesConfiguredConfiguredYesConfigured

Writes and method calls both change equipment, and it's easy to lock down one path and forget the other. If the HMI moves a setpoint with Write but the MES downloads a recipe with Call, both need the role check and the audit record. Never expose StartCycle, DownloadRecipe, or ResetFaults without checking the role inside the server or the equipment layer — hiding the button on the HMI is not access control, because the node is still writable by anyone who knows the NodeId.

Every controlled action needs more than a role

A role check alone isn't enough for anything that touches the process. For each write or method call I want defined, up front:

  • the required role,
  • the required equipment mode or ownership state (a valid role at the wrong time is still dangerous),
  • the allowed value or argument range,
  • the expected StatusCode and error text,
  • the audit fields to capture,
  • and whether a failure should raise an alarm.

The server or equipment layer enforces all of it. The client only decides what to offer.

Make a rejection say why it was rejected

A secure system should fail out loud. When a write bounces, the engineer needs to tell role, mode, range, bad node, certificate, and stale session apart — and OPC UA already hands you distinct StatusCodes for exactly that:

  • Bad_UserAccessDenied — the role/permission check failed.
  • Bad_NotWritable — the node is never writable.
  • Bad_OutOfRange — the value failed a range check.
  • Bad_SecurityChecksFailed — a security or certificate problem, before authorization even mattered.
  • Bad_SessionClosed — stale session, reconnect needed.

The HMI's job is to translate the ones that matter into something an operator can act on. Write failed is useless. HmiViewer role cannot write Mixer M-101 Speed SP tells them it's a permissions issue and points at the tag. That single distinction — a Bad_UserAccessDenied versus a Bad_SecurityChecksFailed — is what separates "your role is wrong" from "your certificate is wrong," and they have completely different fixes.

What the audit event has to reconstruct

For writes, method calls, alarm acknowledgements, and permission failures, log enough to rebuild the action months later:

  • server and source timestamps (when available),
  • session name, application URI, endpoint, client IP,
  • user identity and the mapped role,
  • NodeId, browse path, display name,
  • operation type (read/write/call/acknowledge/condition-refresh),
  • previous value, requested value, and result for writes,
  • method arguments and result for calls — minus anything secret.

Never log passwords, tokens, or private-key material. The audit trail explains actions; it is not a secret dump. And if it records only the application name and not the session's user identity, you have a log that can't answer the one question an audit exists to answer.

Certificate trust and role mapping fail differently

User authentication does not replace application certificate trust — you commission both, together. Before startup, confirm the server trusts the client cert and vice versa, the endpoint URI expectations are written down, expiry dates are known ahead of time, rejected certificates get reviewed rather than blindly moved to the trust list, and test and production certificates aren't mixed without a reason. Backup and restore has to include the certificate stores and, where appropriate, the private keys, or a server rebuild loses trust for every client at once.

When a client reports "authentication failed," the first move is to place the failure on the timeline: did it fail before user login (certificate trust, surfaces as Bad_SecurityChecksFailed) or after (role mapping, surfaces as Bad_UserAccessDenied)? Everything downstream depends on that.

The failures that actually show up

  • The historian collector runs under an operator account and quietly inherits write permission.
  • The MES gateway can browse recipe nodes but can't Call the download method.
  • An HMI popup hides the command button, but the node stays writable by the same user.
  • Anonymous read survives FAT because a test client needed it and nobody turned it off.
  • Password rotation kills exactly one redundant HMI node, because that node stored the credential locally.
  • The audit log has the application name but not the session's user identity.
  • An engineering tool caches an old credential and masks the real permission test — you "confirm" access that the current config wouldn't actually grant.
  • A certificate renewal gets blamed for what was really a role-mapping change, or the reverse.

Almost all of that comes from collapsing application trust, session authentication, and authorization into one word.

A commissioning sequence you can rerun

Before anyone calls the interface ready, run a small repeatable test — and keep it, because you'll want it again after the next certificate renewal:

  1. Connect with an untrusted client certificate; confirm rejection (Bad_SecurityChecksFailed).
  2. Trust the intended cert, connect as a read-only user.
  3. Confirm browse and read work on allowed nodes.
  4. Attempt a setpoint write as the read-only user; confirm Bad_UserAccessDenied.
  5. Connect as the real HMI or service account.
  6. Write an allowed test setpoint; verify the value, the audit event, and the StatusCode.
  7. Attempt an out-of-range write; confirm a clean Bad_OutOfRange.
  8. Attempt a command or method call without the required role; confirm rejection.
  9. Confirm the server logged identity, role, node, operation, and result.
  10. Rotate the test account's credential and confirm reconnect behaves.

Keep a turnover record with the endpoint URL, security policy, identity type, role mapping, certificate thumbprints, and these test results. The day a certificate expires or a server gets replaced, that page is the difference between a ten-minute fix and half a shift of guessing.

One rule that keeps the layers straight: certificates decide which applications may connect, user identity and roles decide what a session may do, equipment logic decides whether the action is safe right now, and audit events prove what happened after the fact. When a write gets rejected and you already know which of those four it was, you're most of the way to the fix.