# 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=,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 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. ## 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=) — 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: 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`. ## Adding a gw-specific group (when reuse isn't enough) If mxaccessgw needs a permission that doesn't fit the existing five roles (e.g. `GwAdmin` for shutdown/recycle commands), add it to GLAuth rather than running a separate LDAP server: 1. Edit `C:\publish\glauth\glauth.cfg` 2. Append: ```toml [[groups]] name = "GwAdmin" gidnumber = 5510 # pick the next free GID ``` 3. Add the group to whichever existing user(s) should have it via `othergroups = [..., 5510]`. Or create a new user: ```toml [[users]] name = "gwadmin" givenname = "Gateway" sn = "Admin" mail = "gwadmin@lmxopcua.local" uidnumber = 5010 primarygroup = 5510 passsha256 = "" ``` 4. `nssm restart GLAuth` 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 directory is reachable: ```powershell # 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: ```bash 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. ## Service management ```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` | `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.