Files
Joseph Doherty 22370ca4da docs(glauth): repoint glauth.md at the shared GLAuth on 10.100.0.35
No more per-box C:\publish\glauth NSSM service — dev/test LDAP is the shared
zb-shared-glauth on 10.100.0.35:3893 (dc=zb,dc=local). Provisioning now via
scadaproj/infra/glauth/config.toml. Old localhost/NSSM procedures kept as
retired reference; test users multi-role/gw-viewer.
2026-06-04 16:38:24 -04:00

13 KiB

GLAuth — LDAP authn reference for mxaccessgw

UPDATED 2026-06-04 — mxaccessgw no longer uses a per-box GLAuth at C:\publish\glauth. Dev/test LDAP is now the SHARED GLAuth on 10.100.0.35:3893 (dc=zb,dc=local); the single source of truth is scadaproj/infra/glauth/ (config.toml + README). The localhost/NSSM/glauth.cfg procedures below are RETIRED, kept for reference/rollback.

GLAuth is a lightweight LDAP server. It already backs all three sister apps (MxAccessGateway, OtOpcUa, ScadaBridge) through a shared container (zb-shared-glauth) running on the Linux docker host at 10.100.0.35:3893. This doc captures everything mxaccessgw needs to consume that directory so a single set of dev credentials covers all stacks.

GLAuth is installed on this dev box at C:\publish\glauth\ and run as a Windows service via NSSM. (RETIRED — the per-box Windows service has been stopped and set to Manual startup; kept only as a rollback option. Do not edit or restart it for new work.)

The single source of truth for the shared GLAuth is ~/Desktop/scadaproj/infra/glauth/config.toml (deploy/verify runbook: scadaproj/infra/glauth/README.md). This doc is a redistilled view tailored to mxaccessgw — what users + groups are provisioned, how to bind against them, and what's needed to add a gw-specific role.

Connection details

Setting Value
Protocol LDAP (unencrypted)
Host 10.100.0.35 (shared docker host — localhost retired)
Port 3893
LDAPS disabled in dev (Transport=None, AllowInsecure=true)
Base DN dc=zb,dc=local
Bind DN format cn={username},dc=zb,dc=local
Service account DN cn=serviceaccount,dc=zb,dc=local / serviceaccount123
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 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 uses two gateway-specific groups beyond the LmxOpcUa taxonomy: GwAdmin (gid 5610 → role Administrator) and GwReader (gid 5611 → role Viewer). These are already provisioned in the shared scadaproj/infra/glauth/config.toml. The dashboard test users are multi-role/password (Administrator) and gw-viewer/password (Viewer). LdapOptions.RequiredGroup defaults to GwAdmin. See Provisioning the GwAdmin group below for the (now-retired) per-box procedure and for the shared-config equivalent.

Dashboard role value (Task 1.7): the LDAP GwAdmin group now maps to the canonical dashboard role Administrator (was Admin); GwReader maps to Viewer. This is a pure value rename via MxGateway:Dashboard:GroupToRole — same operations are authorized. (This dashboard role is distinct from the lowercase gRPC admin API-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

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

ldap:
  enabled: true
  server: 10.100.0.35     # shared GLAuth on docker host (was localhost)
  port: 3893
  useTls: false
  allowInsecureLdap: true       # dev only
  searchBase: "dc=zb,dc=local"
  serviceAccountDn: "cn=serviceaccount,dc=zb,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=zb,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

UPDATED 2026-06-04 — RETIRED per-box procedure. GwAdmin (gid 5610) and GwReader (gid 5611) are already present in the shared GLAuth. To add or modify users/groups, edit ~/Desktop/scadaproj/infra/glauth/config.toml on host 10.100.0.35 and run:

cd ~/Desktop/scadaproj/infra/glauth
docker compose up -d --force-recreate

The per-box C:\publish\glauth\glauth.cfg + NSSM procedure below is kept for rollback reference only — do not use it for new provisioning.

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 logins unless the user is a member of GwAdmin. The GwAdmin (gid 5610) and GwReader (gid 5611) groups already exist in the shared config at scadaproj/infra/glauth/config.toml. Dashboard test users are multi-role/password (Administrator) and gw-viewer/password (Viewer).


RETIRED — per-box provisioning (reference/rollback only):

  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=zb,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 shared directory is reachable:

# Plain bind via PowerShell + System.DirectoryServices.Protocols
# (shared GLAuth on 10.100.0.35 — was localhost, now the docker host)
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("10.100.0.35:3893")
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
$ldap.SessionOptions.ProtocolVersion = 3
$ldap.SessionOptions.SecureSocketLayer = $false
$cred = New-Object System.Net.NetworkCredential("cn=multi-role,dc=zb,dc=local","password")
$ldap.Bind($cred)
"Bind OK"

Or via ldapsearch if you have OpenLDAP CLI tools:

ldapsearch -x -H ldap://10.100.0.35:3893 \
  -D "cn=serviceaccount,dc=zb,dc=local" -w serviceaccount123 \
  -b "dc=zb,dc=local" "(uid=multi-role)"

The response should list multi-role's entry with memberOf including ou=GwAdmin,ou=groups,dc=zb,dc=local.

Service management

RETIRED — per-box NSSM service (reference/rollback only). The shared GLAuth is managed via docker compose on 10.100.0.35 (scadaproj/infra/glauth/). The Windows NSSM GLAuth service on the dev box has been stopped and set to StartupType=Manual; only restart it if you need to roll back to a local directory.

Active (shared) management:

ssh 10.100.0.35
cd ~/Desktop/scadaproj/infra/glauth
docker compose ps                      # check container status
docker compose up -d --force-recreate  # apply config.toml changes
docker compose logs -f                 # tail logs

RETIRED — per-box NSSM commands (rollback reference):

# 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 10.100.0.35 (shared docker host) 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=zb,dc=local DC=corp,DC=example,DC=com
ServiceAccountDn cn=serviceaccount,dc=zb,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 config.toml are dev-only. The shared config is in scadaproj/infra/glauth/config.toml (unencrypted); restrict filesystem access on 10.100.0.35 accordingly. Treat the dev creds as throwaway. Production LDAP is Active Directory. (The retired per-box C:\publish\glauth\glauth.cfg has the same caveat.)
  • 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. The shared GLAuth listens only on the LAN (10.100.0.35); never expose port 3893 externally without enabling TLS first.