Resolve audit findings: correct WorkerEnvelope proto/route/metric/session facts; rewrite auth (ZB.MOM.WW.Auth migration), dashboard (ZB.MOM.WW.Theme), and StyleGuide (foreign-project copy-paste); document alarm subsystem, Ldap options, and gateway alarm broker; fix client CLI flags and package paths.
12 KiB
GLAuth — LDAP authn reference for mxaccessgw
GLAuth is a lightweight LDAP server installed on this dev box at
C:\publish\glauth\ and run as a Windows service via NSSM. It already
backs the LmxOpcUa OPC UA server's UserName-token authn and the LmxOpcUa
Admin UI's cookie login; this doc captures everything mxaccessgw needs
to consume the same directory so a single set of dev credentials covers
both stacks.
The authoritative copy of LmxOpcUa's reference lives at
C:\publish\glauth\auth.md. This doc is a redistilled view tailored to
mxaccessgw — what users + groups are already provisioned, how to bind
against them, and what's needed to add a gw-specific role.
Connection details
| Setting | Value |
|---|---|
| Protocol | LDAP (unencrypted) |
| Host | localhost |
| Port | 3893 |
| LDAPS | disabled in dev (set [ldaps] block to enable) |
| Base DN | dc=zb,dc=local |
| Bind DN format | cn={username},dc=zb,dc=local |
| Group OU | ou=<groupname>,ou=groups,dc=zb,dc=local |
| Failed-bind throttle | 3 fails → 10-minute IP lockout (per [behaviors]) |
Pre-existing groups (LmxOpcUa role taxonomy)
These map cleanly onto MxAccess capability boundaries — mxaccessgw should reuse them rather than define parallel groups so an operator with LmxOpcUa write rights doesn't need a second account for the gw.
| Group | GID | DN | LmxOpcUa meaning | Suggested mxgw mapping |
|---|---|---|---|---|
| ReadOnly | 5501 | ou=ReadOnly,ou=groups,dc=zb,dc=local |
Browse + read OPC UA nodes | Browse + Subscribe (read paths only) |
| WriteOperate | 5502 | ou=WriteOperate,ou=groups,dc=zb,dc=local |
Write FreeAccess / Operate attrs | Write (plain) |
| WriteTune | 5504 | ou=WriteTune,ou=groups,dc=zb,dc=local |
Write Tune attrs | WriteSecured (Tune only) |
| WriteConfigure | 5505 | ou=WriteConfigure,ou=groups,dc=zb,dc=local |
Write Configure attrs | WriteSecured (Configure) |
| AlarmAck | 5503 | ou=AlarmAck,ou=groups,dc=zb,dc=local |
Acknowledge alarms | gw alarm-ack RPC, when added |
A user can be in multiple groups — othergroups = [...] in the
config is a list. admin is the canonical example (in every role
group below).
Pre-provisioned users
| Username | Password | UID | Primary group | Other groups | Capabilities |
|---|---|---|---|---|---|
readonly |
readonly123 |
5001 | ReadOnly | — | Browse, read |
writeop |
writeop123 |
5002 | WriteOperate | — | + plain Write |
writetune |
writetune123 |
5005 | WriteTune | — | + WriteSecured (Tune) |
writeconfig |
writeconfig123 |
5006 | WriteConfigure | — | + WriteSecured (Configure) |
alarmack |
alarmack123 |
5003 | AlarmAck | — | Alarm acknowledgment |
admin |
admin123 |
5004 | ReadOnly | WriteOperate, AlarmAck, WriteTune, WriteConfigure | All roles |
serviceaccount |
serviceaccount123 |
5999 | ReadOnly | — | LDAP search capability (for bind-then-search) |
For mxaccessgw dev, admin covers every gw-side capability test;
readonly is the right "negative" case for proving Browse-OK /
Write-denied.
The gateway dashboard adds one group beyond this LmxOpcUa taxonomy:
GwAdmin. There is no RequiredGroup option — dashboard authorization
is driven entirely by MxGateway:Dashboard:GroupToRole, which maps an
LDAP group to a dashboard role. A user whose groups produce no mapped
role is rejected at login. So for the dashboard to admit admin, a
group named in GroupToRole (by convention GwAdmin → Administrator)
must exist and admin must belong to it. GwAdmin is not in the
baseline GLAuth config — it must be provisioned before dashboard authn
or the DashboardLdapLiveTests (MXGATEWAY_RUN_LIVE_LDAP_TESTS=1)
work. See Provisioning the GwAdmin group
below.
Dashboard role value (Task 1.7): the LDAP
GwAdmingroup now maps to the canonical dashboard roleAdministrator(wasAdmin);GwReadermaps toViewer. This is a pure value rename viaMxGateway:Dashboard:GroupToRole— same operations are authorized. (This dashboard role is distinct from the lowercase gRPCadminAPI-key scope.)
Two bind patterns
1. Direct bind (simplest)
DN: cn=admin,dc=zb,dc=local
Password: admin123
Construct the DN from the username; bind. Works on GLAuth because
backend.nameformat = "cn" and groupformat = "ou" are set in the
config. Doesn't translate to Active Directory — AD users are keyed
by sAMAccountName, not cn. Use this only for dev convenience.
2. Bind-then-search (production-grade)
1. Bind as the service account (cn=serviceaccount,dc=zb,dc=local
/ serviceaccount123).
2. Search under dc=zb,dc=local with filter
(uid=<entered-username>) — or any attribute the deployment
identifies users by. GLAuth populates uid + cn.
3. Read the returned entry's DN + memberOf list (groups).
4. Bind again as the discovered DN with the entered password. If that
succeeds, authn passes; the memberOf values become the role set.
The second bind is the actual password check — the search is just a DN
discovery. This is the AD-friendly path: AD's
tokenGroups / LDAP_MATCHING_RULE_IN_CHAIN flatten nested groups, but
that's an enhancement, not required for first-pass dev.
LmxOpcUa's Server/Security/LdapUserAuthenticator.cs ships a working
implementation of this pattern using Novell.Directory.Ldap.NETStandard
v3.6.0 — copy the bind-then-search loop from there if mxaccessgw wants
to avoid re-deriving the LDAP escape-string handling.
Suggested mxgw configuration shape
The gateway binds the MxGateway:Ldap section onto LdapOptions. The
field names are PascalCase config keys (shown here as YAML; JSON
appsettings and env-var overrides use the same names). Note the keys
that changed from the older LmxOpcUa shape: Transport (an enum,
replacing the boolean UseTls), AllowInsecure (replacing
AllowInsecureLdap), and UserNameAttribute which defaults to cn:
MxGateway:
Ldap:
Enabled: true
Server: localhost
Port: 3893
Transport: None # None | StartTls | Ldaps (dev: None)
AllowInsecure: true # dev only
SearchBase: "dc=zb,dc=local"
ServiceAccountDn: "cn=serviceaccount,dc=zb,dc=local"
ServiceAccountPassword: "serviceaccount123"
UserNameAttribute: "cn" # GLAuth keys users by cn; AD uses sAMAccountName
DisplayNameAttribute: "cn"
GroupAttribute: "memberOf"
Dashboard:
GroupToRole:
GwAdmin: "Administrator"
GwReader: "Viewer"
Transport is an LdapTransport enum (None, StartTls, Ldaps); it
replaces the old boolean UseTls (true ≈ Ldaps, false = None).
UserNameAttribute defaults to cn because GLAuth keys users by cn
(backend.nameformat = "cn"); only AD needs sAMAccountName. The
group-to-role mapping lives under MxGateway:Dashboard:GroupToRole, not
in the LDAP section, and its values must be dashboard roles
(Administrator or Viewer).
The shared ZB.MOM.WW.Auth.Ldap provider performs the runtime bind and
search; it returns each group already stripped to its short RDN value
(e.g. GwAdmin from ou=GwAdmin,ou=groups,dc=zb,dc=local) before the
gateway looks it up in GroupToRole. Keep GroupToRole keys as short
group names — a full-DN key will never match the short name the provider
returns.
Provisioning the GwAdmin group
GwAdmin is the gateway-specific dashboard-admin group, mapped to the
Administrator role through MxGateway:Dashboard:GroupToRole. Because
dashboard login rejects any user who resolves to no role, the dashboard
cookie login and DashboardLdapLiveTests
(MXGATEWAY_RUN_LIVE_LDAP_TESTS=1) reject admin until a GwAdmin
group exists, admin is a member, and GroupToRole maps GwAdmin to a
role. GLAuth's baseline config ships only the five LmxOpcUa role groups,
so GwAdmin must be added to GLAuth rather than run from a separate LDAP
server:
- Edit
C:\publish\glauth\glauth.cfg - Append the group:
[[groups]]
name = "GwAdmin"
gidnumber = 5510 # pick the next free GID
- Add
5510toadmin'sothergroupslist soadminresolves theGwAdminrole. Add it to any other user that needs dashboard-admin rights. Or create a dedicated user:
[[users]]
name = "gwadmin"
givenname = "Gateway"
sn = "Admin"
mail = "gwadmin@lmxopcua.local"
uidnumber = 5010
primarygroup = 5510
passsha256 = "<sha256 of the password — see below>"
nssm restart GLAuth
After the restart, admin's memberOf includes
ou=GwAdmin,ou=groups,dc=zb,dc=local. The shared LDAP provider strips
that to the short RDN GwAdmin, which the gateway looks up in
MxGateway:Dashboard:GroupToRole to resolve the dashboard role. The same
pattern applies to any future group that doesn't fit the existing five
roles — add the group, add the member, and add a GroupToRole entry.
Generate passsha256 from a plaintext password:
# Windows / PowerShell
$bytes = [System.Text.Encoding]::UTF8.GetBytes("yourpassword")
$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
-join ($hash | ForEach-Object { $_.ToString("x2") })
# WSL / git-bash
echo -n "yourpassword" | openssl dgst -sha256
Quick verification
From mxaccessgw's dev box, prove the directory is reachable:
# Plain bind via PowerShell + System.DirectoryServices.Protocols
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("localhost:3893")
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
$ldap.SessionOptions.ProtocolVersion = 3
$ldap.SessionOptions.SecureSocketLayer = $false
$cred = New-Object System.Net.NetworkCredential("cn=admin,dc=zb,dc=local","admin123")
$ldap.Bind($cred)
"Bind OK"
Or via ldapsearch if you have OpenLDAP CLI tools:
ldapsearch -x -H ldap://localhost:3893 \
-D "cn=admin,dc=zb,dc=local" -w admin123 \
-b "dc=zb,dc=local" "(uid=admin)"
The response should list admin's entry with memberOf populated for
all five role groups — plus GwAdmin once the gateway-specific group
is provisioned.
Service management
# Status / start / stop / restart
nssm status GLAuth
nssm start GLAuth
nssm stop GLAuth
nssm restart GLAuth
# Inspect what NSSM was told to launch
nssm get GLAuth Parameters
Logs:
| File | Purpose |
|---|---|
C:\publish\glauth\logs\stdout.log |
Bind events, search responses |
C:\publish\glauth\logs\stderr.log |
Startup errors, config parse failures |
After editing glauth.cfg, always tail stderr.log after the restart
to catch a fat-fingered TOML before it bites at first bind:
nssm restart GLAuth
Get-Content C:\publish\glauth\logs\stderr.log -Tail 20 -Wait
Active Directory migration cheat-sheet
These MxGateway:Ldap keys change when pointing the gateway at AD
instead of dev GLAuth:
| Field | GLAuth dev value | AD production value |
|---|---|---|
Server |
localhost |
a domain controller FQDN, or the domain itself |
Port |
3893 |
636 (LDAPS) — AD increasingly rejects plain bind under LDAP-signing enforcement |
Transport |
None |
Ldaps (or StartTls) |
AllowInsecure |
true |
false |
SearchBase |
dc=zb,dc=local |
DC=corp,DC=example,DC=com |
ServiceAccountDn |
cn=serviceaccount,dc=zb,dc=local |
CN=MxGwSvc,OU=Service Accounts,DC=corp,... |
UserNameAttribute |
cn |
sAMAccountName (or userPrincipalName) |
GroupAttribute |
memberOf (unchanged) |
memberOf (unchanged) |
memberOf returns full DNs; the shared LDAP provider strips each to its
leading RDN value (CN=/OU=) and the gateway uses that as the lookup
key in MxGateway:Dashboard:GroupToRole. Nested groups are not
auto-expanded; either flatten in the directory or add a tokenGroups
query as an enhancement.
Security notes for production
- Plaintext passwords in
glauth.cfgare dev-only. The config is unencrypted on disk; anyone with read access toC:\publish\glauth\can SHA256-rainbow-table the entries. Treat the dev creds as throwaway. Production LDAP is Active Directory. - The 3-fail / 10-minute lockout is per source IP, not per user — a
shared NAT can lock out a whole office. Tunable in
[behaviors]. - LDAPS isn't enabled in dev; binding sends passwords cleartext on the
wire. Fine for
localhost, never expose port 3893 off-box without enabling TLS first.