22370ca4da
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.
328 lines
13 KiB
Markdown
328 lines
13 KiB
Markdown
# 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 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=<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:
|
|
|
|
```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 = "<sha256 of the password — see below>"
|
|
```
|
|
|
|
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.
|