59bf74760c
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
122 lines
6.8 KiB
Markdown
122 lines
6.8 KiB
Markdown
# 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`** — 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`](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`](../../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.
|
|
|
|
```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. 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`):
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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:
|
|
|
|
```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 '<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):
|
|
|
|
```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.
|