From 0f2b2b8351a1a8fcc157bb6e217d4b7b0aa31e0f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 4 Jun 2026 15:45:41 -0400 Subject: [PATCH] feat(glauth): merged shared dev GLAuth directory + compose + runbook (10.100.0.35) Phase 0 of the shared-GLAuth standardization. config.toml = merged dc=zb,dc=local directory (15 groups in partitioned 55xx/56xx/57xx families, 14 users incl. multi-role spanning all groups, serviceaccount search account). compose runs one glauth/glauth:latest on :3893. README is the deploy/verify runbook. Code-reviewed; fixed scp -r idempotency in the deploy command (README + plan Task 4). --- ...026-06-04-shared-glauth-standardization.md | 10 +- infra/glauth/README.md | 88 ++++++++++ infra/glauth/config.toml | 165 ++++++++++++++++++ infra/glauth/docker-compose.yml | 15 ++ 4 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 infra/glauth/README.md create mode 100644 infra/glauth/config.toml create mode 100644 infra/glauth/docker-compose.yml diff --git a/docs/plans/2026-06-04-shared-glauth-standardization.md b/docs/plans/2026-06-04-shared-glauth-standardization.md index 0d17de1..1d3dc53 100644 --- a/docs/plans/2026-06-04-shared-glauth-standardization.md +++ b/docs/plans/2026-06-04-shared-glauth-standardization.md @@ -281,10 +281,14 @@ git commit -m "feat(glauth): merged shared dev GLAuth directory + compose + runb **Step 1: Resolve access.** Confirm `ssh dohertj2@10.100.0.35 'echo ok'` works. If it does NOT (currently the case from this Mac), STOP and either (a) have the user re-authorize this Mac's key on 35, or (b) hand the user `infra/glauth/` + the deploy command to run on 35. Do not proceed past this gate until GLAuth is up on 35. -**Step 2: Deploy** (once access works): +**Step 2: Deploy** (once access works). Copy the FILES into the dest dir (not the dir itself) so a +re-deploy doesn't nest them at `~/zb-glauth/glauth/` (the `scp -r dir-into-existing-dir` trap): ```bash -scp -r /Users/dohertj2/Desktop/scadaproj/infra/glauth dohertj2@10.100.0.35:~/zb-glauth -ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d && docker ps --filter name=zb-shared-glauth' +ssh dohertj2@10.100.0.35 'mkdir -p ~/zb-glauth' +scp /Users/dohertj2/Desktop/scadaproj/infra/glauth/config.toml \ + /Users/dohertj2/Desktop/scadaproj/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' ``` Expected: `zb-shared-glauth` container `Up`. diff --git a/infra/glauth/README.md b/infra/glauth/README.md new file mode 100644 index 0000000..4b0e749 --- /dev/null +++ b/infra/glauth/README.md @@ -0,0 +1,88 @@ +# Shared dev GLAuth (`dc=zb,dc=local`) + +One [GLAuth](https://github.com/glauth/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:3893`** (plaintext LDAP). This is the app-neutral source of +truth; each app just points its `…Ldap:Server` at `10.100.0.35`. + +> Scope: **dev/test only**. Production uses real corporate AD. See +> [`../../docs/plans/2026-06-04-shared-glauth-standardization-design.md`](../../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` → `serviceaccount123`): + +- **`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); `gwreader` + (MxGateway Viewer); `otdesigner` / `otviewer` (OtOpcUa); `readonly` / `writeop` / `writetune` + / `writeconfig` / `alarmack` (OPC perms). + +## 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. + +```bash +# 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: + +```bash +ldapsearch -x -H ldap://10.100.0.35:3893 \ + -D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \ + -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): + +```bash +ldapsearch -x -H ldap://10.100.0.35:3893 \ + -D cn=multi-role,dc=zb,dc=local -w password \ + -b dc=zb,dc=local "(cn=multi-role)" cn +``` + +No `ldapsearch` locally? Run it from a throwaway container: + +```bash +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 serviceaccount123 \ + -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): + +```bash +docker compose up -d --force-recreate +``` + +Group `gidnumber`s and user `uidnumber`s must stay **unique** across the whole file; keep new +groups inside the per-app range (55xx / 56xx / 57xx) so the families don't collide. diff --git a/infra/glauth/config.toml b/infra/glauth/config.toml new file mode 100644 index 0000000..c044936 --- /dev/null +++ b/infra/glauth/config.toml @@ -0,0 +1,165 @@ +[ldap] + enabled = true + listen = "0.0.0.0:3893" + +[ldaps] + enabled = false + +[backend] + datastore = "config" + baseDN = "dc=zb,dc=local" + +[behaviors] + # Dev: do not lock out on failed binds (avoids surprises during testing). + LimitFailedBinds = false + +# ── Groups ─────────────────────────────────────────────────────────── +# ScadaBridge role groups (55xx) — DB-mapped (LdapGroupMappings) +[[groups]] + name = "SCADA-Admins" + gidnumber = 5501 +[[groups]] + name = "SCADA-Designers" + gidnumber = 5502 +[[groups]] + name = "SCADA-Deploy-All" + gidnumber = 5503 +[[groups]] + name = "SCADA-Deploy-SiteA" + gidnumber = 5504 +[[groups]] + name = "SCADA-Viewers" + gidnumber = 5505 + +# OPC-UA permission groups (560x) — OtOpcUa + MxGateway OPC write model +[[groups]] + name = "ReadOnly" + gidnumber = 5601 +[[groups]] + name = "WriteOperate" + gidnumber = 5602 +[[groups]] + name = "WriteTune" + gidnumber = 5603 +[[groups]] + name = "WriteConfigure" + gidnumber = 5604 +[[groups]] + name = "AlarmAck" + gidnumber = 5605 + +# MxGateway dashboard groups (561x) — config-mapped (GroupToRole) +[[groups]] + name = "GwAdmin" + gidnumber = 5610 +[[groups]] + name = "GwReader" + gidnumber = 5611 + +# OtOpcUa AdminUI role groups (57xx) — DB-mapped (LdapGroupRoleMapping) +[[groups]] + name = "OtOpcUa-Admins" + gidnumber = 5701 +[[groups]] + name = "OtOpcUa-Designers" + gidnumber = 5702 +[[groups]] + name = "OtOpcUa-Viewers" + gidnumber = 5703 + +# ── Users ──────────────────────────────────────────────────────────── +# All passwords are "password" except serviceaccount ("serviceaccount123"). +# sha256("password") = 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 +# sha256("serviceaccount123") = af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419 + +# The single bind account every app uses (search-then-bind). +[[users]] + name = "serviceaccount" + uidnumber = 5999 + primarygroup = 5601 + passsha256 = "af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419" + [[users.capabilities]] + action = "search" + object = "*" + +# Cross-app: member of EVERY group → all roles in all three apps. +[[users]] + name = "multi-role" + givenname = "Multi" + sn = "Role" + mail = "multi-role@zb.local" + uidnumber = 5005 + primarygroup = 5501 + othergroups = [5502, 5503, 5504, 5505, 5601, 5602, 5603, 5604, 5605, 5610, 5611, 5701, 5702, 5703] + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + +# Administrator everywhere (admin-equivalent of each app). +[[users]] + name = "admin" + uidnumber = 5001 + primarygroup = 5501 + othergroups = [5610, 5701] + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + +# ScadaBridge single-role testers +[[users]] + name = "designer" + uidnumber = 5002 + primarygroup = 5502 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "deployer" + uidnumber = 5003 + primarygroup = 5503 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "site-deployer" + uidnumber = 5004 + primarygroup = 5504 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + +# MxGateway dashboard Viewer tester +[[users]] + name = "gwreader" + uidnumber = 5106 + primarygroup = 5611 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + +# OPC-UA permission testers +[[users]] + name = "readonly" + uidnumber = 5101 + primarygroup = 5601 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "writeop" + uidnumber = 5102 + primarygroup = 5602 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "writetune" + uidnumber = 5103 + primarygroup = 5603 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "writeconfig" + uidnumber = 5104 + primarygroup = 5604 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "alarmack" + uidnumber = 5105 + primarygroup = 5605 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + +# OtOpcUa single-role testers (admin covers OtOpcUa-Admins) +[[users]] + name = "otdesigner" + uidnumber = 5202 + primarygroup = 5702 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" +[[users]] + name = "otviewer" + uidnumber = 5203 + primarygroup = 5703 + passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" diff --git a/infra/glauth/docker-compose.yml b/infra/glauth/docker-compose.yml new file mode 100644 index 0000000..a1f8f85 --- /dev/null +++ b/infra/glauth/docker-compose.yml @@ -0,0 +1,15 @@ +# Shared dev GLAuth for OtOpcUa + MxAccessGateway + ScadaBridge. +# Deploy on the shared Docker host 10.100.0.35: docker compose up -d +# Verify: ldapsearch -x -H ldap://10.100.0.35:3893 \ +# -D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \ +# -b dc=zb,dc=local "(cn=multi-role)" memberOf +name: zb-shared-glauth +services: + glauth: + image: glauth/glauth:latest + container_name: zb-shared-glauth + restart: unless-stopped + ports: + - "3893:3893" + volumes: + - ./config.toml:/app/config/config.cfg:ro