Files
mxaccessgw/glauth.md
Joseph Doherty bc55396334 Resolve IntegrationTests-001 and IntegrationTests-002 code-review findings
IntegrationTests-001: documented the live Galaxy Repository test suite and
its MXGATEWAY_RUN_LIVE_GALAXY_TESTS / MXGATEWAY_LIVE_GALAXY_CONN gating in
docs/GatewayTesting.md.

IntegrationTests-002: documented the live LDAP test suite in
docs/GatewayTesting.md and added a concrete "Provisioning the GwAdmin group"
step to glauth.md so DashboardLdapLiveTests' GwAdmin-membership assumption
is reproducible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:46:09 -04:00

10 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=lmxopcua,dc=local
Bind DN format cn={username},dc=lmxopcua,dc=local
Group OU ou=<groupname>,ou=groups,dc=lmxopcua,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=lmxopcua,dc=local Browse + read OPC UA nodes Browse + Subscribe (read paths only)
WriteOperate 5502 ou=WriteOperate,ou=groups,dc=lmxopcua,dc=local Write FreeAccess / Operate attrs Write (plain)
WriteTune 5504 ou=WriteTune,ou=groups,dc=lmxopcua,dc=local Write Tune attrs WriteSecured (Tune only)
WriteConfigure 5505 ou=WriteConfigure,ou=groups,dc=lmxopcua,dc=local Write Configure attrs WriteSecured (Configure)
AlarmAck 5503 ou=AlarmAck,ou=groups,dc=lmxopcua,dc=local Acknowledge alarms gw alarm-ack RPC, when added

A user can be in multiple groupsothergroups = [...] 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 role beyond this LmxOpcUa taxonomy: GwAdmin. LdapOptions.RequiredGroup defaults to GwAdmin, so the dashboard login and DashboardLdapLiveTests require admin to be a member of a GwAdmin group. GwAdmin is not in the baseline GLAuth config — it must be provisioned before dashboard authn or the LDAP live tests work. See Provisioning the GwAdmin group below.

Two bind patterns

1. Direct bind (simplest)

DN:       cn=admin,dc=lmxopcua,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=lmxopcua,dc=local
   / serviceaccount123).
2. Search under dc=lmxopcua,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

A YAML/JSON section for mxaccessgw that mirrors LmxOpcUa's LdapOptions record:

ldap:
  enabled: true
  server: localhost
  port: 3893
  useTls: false
  allowInsecureLdap: true       # dev only
  searchBase: "dc=lmxopcua,dc=local"
  serviceAccountDn: "cn=serviceaccount,dc=lmxopcua,dc=local"
  serviceAccountPassword: "serviceaccount123"
  userNameAttribute: "uid"      # GLAuth populates this; AD uses sAMAccountName
  displayNameAttribute: "cn"
  groupAttribute: "memberOf"
  groupToRole:
    ReadOnly: "Browse"
    WriteOperate: "Write"
    WriteTune: "WriteSecured"
    WriteConfigure: "WriteSecured"
    AlarmAck: "AlarmAck"

groupAttribute returns full DNs like ou=ReadOnly,ou=groups,dc=lmxopcua,dc=local — the authenticator should strip the leading ou= (or cn= against AD) RDN value and look that up in groupToRole.

Provisioning the GwAdmin group

GwAdmin is the gateway-specific dashboard-admin role. It is the default LdapOptions.RequiredGroup, so the dashboard cookie login and DashboardLdapLiveTests (MXGATEWAY_RUN_LIVE_LDAP_TESTS=1) reject admin until a GwAdmin group exists and admin is a member. 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:

  1. Edit C:\publish\glauth\glauth.cfg
  2. Append the group:
[[groups]]
  name = "GwAdmin"
  gidnumber = 5510              # pick the next free GID
  1. Add 5510 to admin's othergroups list so admin resolves the GwAdmin role. 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>"
  1. nssm restart GLAuth

After the restart, admin's memberOf includes ou=GwAdmin,ou=groups,dc=lmxopcua,dc=local, which the authenticator strips to GwAdmin and matches against RequiredGroup. The same pattern applies to any future permission that doesn't fit the existing five roles.

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=lmxopcua,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=lmxopcua,dc=local" -w admin123 \
  -b "dc=lmxopcua,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

LmxOpcUa's LdapOptions xml-doc captures the AD overrides; same set applies to mxaccessgw verbatim. Keys that change:

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
UseTls false true
AllowInsecureLdap true false
SearchBase dc=lmxopcua,dc=local DC=corp,DC=example,DC=com
ServiceAccountDn cn=serviceaccount,dc=lmxopcua,dc=local CN=MxGwSvc,OU=Service Accounts,DC=corp,...
UserNameAttribute uid sAMAccountName (or userPrincipalName)
GroupAttribute memberOf (unchanged) memberOf (unchanged)

memberOf returns full DNs; the authenticator strips the leading CN= value and uses it as the lookup key in 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.cfg are dev-only. The config is unencrypted on disk; anyone with read access to C:\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.