# 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=,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 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](#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=) — 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: ```yaml 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: > > ```bash > 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: ```toml [[groups]] name = "GwAdmin" gidnumber = 5510 # pick the next free GID ``` 3. 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: ```toml [[users]] name = "gwadmin" givenname = "Gateway" sn = "Admin" mail = "gwadmin@lmxopcua.local" uidnumber = 5010 primarygroup = 5510 passsha256 = "" ``` 4. `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: ```powershell # Windows / PowerShell $bytes = [System.Text.Encoding]::UTF8.GetBytes("yourpassword") $hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes) -join ($hash | ForEach-Object { $_.ToString("x2") }) ``` ```bash # WSL / git-bash echo -n "yourpassword" | openssl dgst -sha256 ``` ## Quick verification From mxaccessgw's dev box, prove the shared directory is reachable: ```powershell # 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: ```bash 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:** > ```bash > 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):** ```powershell # 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: ```powershell 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.