Why an OPC UA Setpoint That Browses Writable Still Rejects the Operator
AccessLevel vs UserAccessLevel vs WriteMask: why a setpoint that browses writable returns BadUserAccessDenied, and testing with the production identity.
Writable in the browse tool does not mean writable by the operator
A setpoint browses fine in UaExpert. The pencil icon is there, you type a value, it takes. Then the HMI goes live under the SCADA service account and the same node returns BadUserAccessDenied (0x801F0000) on every write. Nothing about the address space changed — the identity did.
OPC UA keeps four attributes here that read almost the same when a project is rushed, and they are not interchangeable. Per OPC UA Part 3, AccessLevel is a byte bitmask of what the node supports at all: CurrentRead is bit 0 (value 1), CurrentWrite is bit 1 (value 2), so a writable value node reads AccessLevel = 3. UserAccessLevel uses the exact same encoding but is filtered for your session — the browse tool logged in as an engineer sees 3, the restricted runtime account sees 1. That single-bit gap is the failure, and a browse tool run under the wrong login hides it completely.
WriteMask and UserWriteMask are a different thing entirely (more on that below), and neither one governs whether you can write the Value.
Do not approve a write path because a browse tool showed a pencil icon. The pencil reflects the browsing session's UserAccessLevel, not the runtime's. Test the same endpoint, security mode, certificate, user, and role the SCADA runtime will use — or you are testing the wrong permission set.
Record access attributes beside the tag mapping
For each operator-writeable item, keep a small access record. It does not need to be a large security document, but it should be close enough to the tag list that engineers can use it during checkout.
| Item | What to record | Why it helps |
|---|---|---|
| NodeId and BrowseName | Exact source node used by the HMI | Prevents writes to a similarly named engineering node |
| Data type | Boolean, Int16, Float, enum, structure | Avoids rejected writes and bad setpoint formatting |
| AccessLevel | Server-level read/write capability | Shows whether the node is intended to accept writes |
| UserAccessLevel | Runtime account capability | Catches role or certificate differences |
| Range and unit | Engineering limits and unit | Prevents valid protocol writes with unsafe values |
| Expected reject reason | Local mode, interlock, locked recipe, bad quality | Helps the HMI show a useful message |
The access record is especially useful when a vendor exposes both command nodes and feedback nodes. Pump101.StartCmd and Pump101.Running may be close together in the address space, but only one should be written.
Treat WriteMask as configuration access, not operator control
A common misunderstanding is to read WriteMask as "can write the process value." It doesn't. In OPC UA Part 3, WriteMask is a 32-bit map over which attributes of a node can be modified — DisplayName, Description, the AccessLevel attribute itself, and so on. The Value attribute has its own dedicated bit, but for a normal Variable the writability of the Value is governed by AccessLevel/UserAccessLevel bit 1 (CurrentWrite), not by WriteMask. So a node can show WriteMask = 0 and still accept value writes all day. Most SCADA clients should never need WriteMask set in production — if it is, someone can rename your tags at runtime.
For operator controls, focus on:
CurrentWritein UserAccessLevel for the runtime session.- Data type and array shape.
- engineering range checks.
- server-side role and certificate rules.
- PLC or equipment state that may reject the command after the OPC UA write request is accepted.
If an HMI needs to change metadata at runtime, treat it as a configuration management feature and audit it separately. Normal start, stop, reset, and setpoint operations should not require metadata writes.
Test with the production identity
Many write failures are identity failures disguised as tag problems. The engineering user can write in the OPC UA test client, but the SCADA service account cannot. Or the HMI works before a certificate replacement and fails afterward because the server maps the certificate to a different role.
During checkout, run a write test matrix:
- Connect with the SCADA runtime user, certificate, endpoint URL, and security policy.
- Read
AccessLevelandUserAccessLevelfor the target node. - Write a safe test value or use a vendor-provided simulation command.
- Confirm feedback from the equipment, not only the OPC UA write result.
- Repeat with an unauthorized user and verify that the write is rejected.
- Review the server audit log for the accepted and rejected attempts.
For a setpoint, include low, normal, high, and out-of-range values. For a command bit, prove that the bit does not remain latched because the client lost connection after writing.
Show protocol rejection differently from process rejection
Operators need to know whether the HMI could not write the tag or whether the equipment rejected the request. Those are different faults. The status code returned by the Write service (OPC UA Part 4, §5.10.4) tells you which layer said no — so surface it, don't swallow it.
| Status code | Likely layer | Useful operator message |
|---|---|---|
BadUserAccessDenied (0x801F0000) | OPC UA user or role | Write rejected: HMI account not permitted |
BadNotWritable (0x803B0000) | OPC UA node access (CurrentWrite clear) | Write rejected: node is read-only |
BadTypeMismatch (0x80730000) | Client value formatting | Write rejected: value type mismatch |
BadOutOfRange (0x803C0000) | Server range validation | Write rejected: value out of range |
Good but feedback does not change | PLC or equipment logic | Command sent, no running feedback |
Good, then reject code in a status tag | Process validation | Command rejected: local mode |
Do not collapse all of these into Command failed. The top group (Bad* status codes) usually needs engineering or security configuration work. The Good-with-no-effect cases are often normal process behavior — local mode, an interlock — that the operator can resolve at the panel.
Common failure modes
The browse account is not the runtime account
An integrator browses with an administrator account and imports tags. The runtime service uses a restricted account. Read tags work, but all setpoints fail after deployment.
Field check: browse and write once from the actual runtime host using the actual runtime credentials.
A replacement server changes role mapping
After an OPC UA server upgrade, the endpoint and NodeIds look the same, but certificate-to-role mapping is reset. The HMI can connect and read, but writes return access denied.
Field check: include user and role mapping in the backup/restore checklist, not only certificates and endpoints.
The HMI writes the feedback node
A template is bound to the wrong node because the command and feedback names are similar. The write may fail cleanly, or worse, it may write a simulation or override node.
Field check: for every command, trace the NodeId back to the vendor interface document or PLC export.
Commissioning evidence to keep
Save enough evidence that the next engineer can tell what was actually tested:
- endpoint URL, security policy, and message mode;
- client certificate thumbprint or application URI;
- runtime user or role used for the test;
- target NodeId, data type, and engineering unit;
- successful write value and resulting feedback;
- rejected write test and returned status code;
- HMI message shown to the operator.
The one test people skip is the negative one: log in as an account that should not be able to write, attempt the setpoint, and confirm you get BadUserAccessDenied and an audit entry. A write path that only ever proves the authorized case is half-tested — you have shown the door opens, not that it locks.