Files
scadaproj/infra/glauth/README.md
T
Joseph Doherty 59bf74760c infra(glauth): enable LDAPS on :3894 alongside plain :3893 (#19, mxaccessgw#180)
Self-signed cert (SAN IP 10.100.0.35), public cert checked in, key host-only.
Deployed on 10.100.0.35: TLS chain verifies with the cert and fails without
it; raw LDAPS simple bind returns 0 for a good password and 49 for a bad one;
plain :3893 bind unchanged. README: thumbprint, client config, rotation; also
fixes the user-bind DN example (GLAuth puts users under their primary group OU).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 11:46:48 -05:00

6.8 KiB

Shared dev GLAuth (dc=zb,dc=local)

One GLAuth directory that all three sister apps use for dev/test auth — OtOpcUa, MxAccessGateway, ScadaBridge. It runs as a single container on the shared Docker host 10.100.0.35 — plain LDAP on :3893 and LDAPS on :3894 (since 2026-11-10, #19). This is the app-neutral source of truth; each app just points its …Ldap:Server at 10.100.0.35.

Open work on this directory is tracked as Gitea issues on dohertj2/scadaproj.

LDAPS (:3894)

LDAPS runs next to plain LDAP; :3893 is unchanged, so every existing Transport=None consumer keeps working.

  • Cert: self-signed, CN=zb-shared-glauth, SAN IP:10.100.0.35, DNS:localhost, DNS:zb-shared-glauth, RSA-3072, valid to 2029-11-09. The public cert is checked in at tls/cert.pem; the private key exists only on the host (~/zb-glauth/tls/key.pem) and is gitignored here.
  • SHA-256 thumbprint (for clients that pin, e.g. mxaccessgw MxGateway:Ldap — mxaccessgw#180): EE:B0:13:70:3F:B8:65:4C:9E:6F:39:0F:22:BE:A1:0B:6D:F5:C6:6E:56:57:84:59:E0:93:C6:07:69:D9:9B:BD (openssl x509 -in tls/cert.pem -noout -fingerprint -sha256).
  • Client config: Transport=Ldaps, server 10.100.0.35, port 3894. A client must either pin the thumbprint or trust tls/cert.pem; it is not in any OS trust store.
  • Verify the chain: echo | openssl s_client -connect 10.100.0.35:3894 -CAfile tls/cert.pem -verify_return_error -verify_ip 10.100.0.35 → Verify return code: 0 (ok); without -CAfile it must fail with code 18 (self-signed). macOS ldapsearch ignores LDAPTLS_CACERT, so test binds from the alpine container below with LDAPTLS_CACERT=/certs/cert.pem and -v $PWD/tls:/certs:ro.
  • Rotating: generate a new pair in ~/zb-glauth/tls/ on the host, copy cert.pem back here, update the thumbprint above, docker compose up -d --force-recreate. Pinning clients should carry old + new thumbprints across the swap.
  • Not covered: the redundant local pair (zb-ldap-primary/zb-ldap-backup on the Mac) and each wonder box's own ScadaBridge-LDAP GLAuth remain plain-only.

Scope: dev/test only. Production uses real corporate AD. See ../../docs/plans/2026-06-04-shared-glauth-standardization-design.md.

Directory layout

Group families are partitioned into non-overlapping gid ranges so the three apps coexist — each app maps only its own family and ignores the rest.

Family Used by Groups (gidnumber)
SCADA-* (55xx) ScadaBridge roles (DB-mapped) Admins 5501, Designers 5502, Deploy-All 5503, Deploy-SiteA 5504, Viewers 5505
OPC-perm (560x) OtOpcUa + MxGateway OPC-UA write model ReadOnly 5601, WriteOperate 5602, WriteTune 5603, WriteConfigure 5604, AlarmAck 5605
Gw* (561x) MxGateway dashboard (config-mapped) GwAdmin 5610, GwReader 5611
OtOpcUa-* (57xx) OtOpcUa AdminUI (DB-mapped) Admins 5701, Designers 5702, Viewers 5703

Users (all password password except serviceaccount, whose password is rotated out-of-band — SEC-36 — and is not recorded in this repo; it lives only in the passsha256 below and each app's secret store):

  • serviceaccount (cn=serviceaccount,dc=zb,dc=local) — the single bind account every app uses for search-then-bind. Has a search * capability.
  • multi-role — member of every group → all roles in all three apps (canonical cross-app login).
  • admin — SCADA-Admins + GwAdmin + OtOpcUa-Admins → Administrator everywhere.
  • Per-role testers: designer / deployer / site-deployer (ScadaBridge); gw-viewer (MxGateway Viewer); otdesigner / otviewer (OtOpcUa); opc-readonly / opc-writeop / opc-writetune / opc-writeconfig / opc-alarmack (OPC perms).

Naming rule: a tester username must not case-collide with a group name. GLAuth exposes each group as cn=<Group> under ou=users, so a case-insensitive (cn=x) search would match both the user and the group (two entries → the shared lib's "exactly one entry" rule fails the bind). That's why the OPC/Gw testers are opc-* / gw-viewer, not readonly / gwreader.

Deploy on 10.100.0.35

Access note: deploying needs working SSH/Docker access to 10.100.0.35. If your key is not authorized there, hand this folder to whoever administers the box and have them run the same docker compose up -d. The artifact is self-contained.

# From this repo root. Copy the FILES into the dest (not the dir) so a re-deploy
# doesn't nest them at ~/zb-glauth/glauth/ (scp -r of a dir into an existing dir).
ssh dohertj2@10.100.0.35 'mkdir -p ~/zb-glauth'
scp infra/glauth/config.toml infra/glauth/docker-compose.yml dohertj2@10.100.0.35:~/zb-glauth/
ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d --force-recreate && docker ps --filter name=zb-shared-glauth'

Verify

Bind as the service account and confirm multi-role spans all four families. Substitute the current service-account password (rotated out-of-band, SEC-36 — take it from the consuming app's secret store, e.g. the gateway's MxGateway:Ldap:ServiceAccountPassword):

ldapsearch -x -H ldap://10.100.0.35:3893 \
  -D cn=serviceaccount,dc=zb,dc=local -w '<service-account-password>' \
  -b dc=zb,dc=local "(cn=multi-role)" memberOf
# → result: 0 Success. memberOf comes back as group DNs (e.g. ou=SCADA-Admins,ou=groups,dc=zb,dc=local)
#   spanning all four families: SCADA-*, ReadOnly/Write*/AlarmAck, GwAdmin/GwReader, OtOpcUa-*.
#   (The shared ZB.MOM.WW.Auth.Ldap lib strips each to its bare RDN, e.g. "SCADA-Admins", at login.)

Confirm a user authenticates with password (a bad password returns result: 49; this user lacks the search capability, so a successful bind shows result: 50 Insufficient access on the search):

ldapsearch -x -H ldap://10.100.0.35:3893 \
  -D cn=multi-role,ou=SCADA-Admins,dc=zb,dc=local -w password \
  -b dc=zb,dc=local "(cn=multi-role)" cn

No ldapsearch locally? Run it from a throwaway container:

docker run --rm alpine:3.20 sh -c 'apk add -q openldap-clients >/dev/null 2>&1 && \
  ldapsearch -x -H ldap://10.100.0.35:3893 -D cn=serviceaccount,dc=zb,dc=local -w '<service-account-password>' \
  -b dc=zb,dc=local "(cn=multi-role)" memberOf'

Editing the directory

GLAuth uses the config datastore (this config.toml, mounted read-only). To add/change a user or group, edit config.toml and recreate the container — a plain restart keeps the stale file (single-file Docker bind-mount inode trap):

docker compose up -d --force-recreate

Group gidnumbers and user uidnumbers must stay unique across the whole file; keep new groups inside the per-app range (55xx / 56xx / 57xx) so the families don't collide.