Files
scadaproj/tools/LdapProbe/README.md
T
Joseph Doherty 4e01cbc2c3 feat(tools): ldapprobe -- NAM AD auth + group discovery probe
Diagnostic for pinning down the LDAP settings the four ZB.MOM.WW apps need
against the Zimmer NAM domain, and for answering which AD groups can be mapped
to app roles. Found the referral defect fixed in 0.2.1.

Two independent halves in one run:

Part A drives the REAL ZB.MOM.WW.Auth.LdapAuthService -- same bind-then-search-
then-bind, same options validator, same failure enum -- so its verdict IS what
the apps produce rather than an approximation of it.

Part B asks the directory what the library cannot, which is where the group
answer comes from: full group DNs, nested groups via
LDAP_MATCHING_RULE_IN_CHAIN, the primary group (AD omits it from memberOf, so
it can never be a role group), security vs distribution type, decoded
userAccountControl, referral counts, and the TLS chain with key sizes.
--list-groups '<pat>' enumerates every matching group in the directory, which
a membership dump structurally cannot do -- it only ever shows one account's
memberships.

Passwords are prompted for and never echoed; they cannot be passed as
arguments, which would put them in shell history. --self-bind lets the user
under test stand in for the service account, so group discovery does not
require the svc_igntnapp secret.

Not packable, outside the Auth solution and its central package management,
referencing the Auth projects directly so it tracks current source.
2026-08-12 21:27:14 -04:00

101 lines
5.3 KiB
Markdown

# ldapprobe — ZB / NAM Active Directory auth + group probe
A throwaway diagnostic for pinning down the LDAP settings the four ZB.MOM.WW apps need
against the Zimmer NAM domain, and for answering the open question in
[`../../ldap_setup.md`](../../ldap_setup.md): **which AD groups should we map to app roles?**
The NAM directory has no established group→role convention (the Ignition gateway assigns
roles per-user inside the application and ignores AD groups entirely), while
`ZB.MOM.WW.Auth.Ldap` **fails closed on zero groups** — a user with a perfectly good
password is refused if `memberOf` comes back empty. This probe exists to close that gap
with data rather than guesswork.
## Running it
```bash
cd ~/Desktop/scadaproj
# no service-account password to hand — bind as the user under test:
dotnet run --project tools/LdapProbe -- --insecure --self-bind <sAMAccountName>
# full fidelity, service account from the environment:
export ZB_LDAP_SVC_PASSWORD='...'
dotnet run --project tools/LdapProbe -- --insecure <sAMAccountName>
# which transports actually reach the DCs from here:
dotnet run --project tools/LdapProbe -- --sweep --insecure -u x
# what UG_WW_* groups exist across the whole directory:
dotnet run --project tools/LdapProbe -- --insecure --self-bind -g 'UG_WW_*' <sAMAccountName>
dotnet run --project tools/LdapProbe -- --help
```
Quote the pattern — an unquoted `UG_WW_*` is glob-expanded by the shell.
Passwords are always prompted for and never echoed. They cannot be passed on the command
line — that would put them in shell history. The service-account password may come from
`$ZB_LDAP_SVC_PASSWORD` instead.
Requires the Zimmer VPN to be up: the DCs are `uswap-vdc-nam1` (10.201.216.2) and
`uswap-vdc-nam2` (10.201.138.78).
## What it reports
**Part A** drives the real `ZB.MOM.WW.Auth.LdapAuthService`, so its verdict is exactly
what ScadaBridge / OtOpcUa / MxGateway / HistorianGateway will produce — same bind-then-
search-then-bind flow, same options validator, same failure enum, same fail-closed rules.
Its five failure values are each explained in place, because `ServiceAccountBindFailed`
in particular collapses three very different causes (connect/TLS, service-account bind,
and the search itself) into one value.
**Part B** asks the directory things the library does not, which is where the group
answer comes from:
| | why it matters |
|---|---|
| full user entry | `userAccountControl` decoded, so a disabled / locked / password-expired account is named rather than showing up as "bad credentials" |
| **direct `memberOf`** | the *only* set the shared library sees |
| **primary group** | AD omits it from `memberOf` — so it can never be used as a role group, and a user whose only group is `Domain Users` will always fail |
| **transitive groups** | resolved server-side via `LDAP_MATCHING_RULE_IN_CHAIN`; a group reachable only by nesting is flagged, because mapping one would silently grant nobody anything |
| group type | `security` vs `distribution`, and scope — decoded from `groupType` |
| referral count | a search based at the domain root makes AD return continuation referrals — the cause of the `ServiceAccountBindFailed` fixed in Auth `0.2.1` |
**`--list-groups <pattern>`** adds a third, independent section: every group in the
directory whose `cn` matches, not just the ones the test account belongs to. A membership
dump structurally cannot answer "what groups exist to choose from" — it only ever shows
one person's memberships, which for a long-tenured account skews heavily toward admin
groups. Each row shows the group type, its member count (`EMPTY` and `N+` for AD's ranged
1500-value cap are both called out) and whether the probed account holds it.
It searches from the **domain root** by default, derived from the resolved user DN, because
groups here live in at least three different containers and a narrower base under-reports
without saying so.
It closes with a ready-to-paste `appsettings` block, and — when the library failed
system-side while a direct query succeeded — automatically retries Part A against the
user's own container to test whether the domain-root search base is the culprit.
## `--insecure`
The DC certificates chain to the internal `zimmer-rootca`, which is not in a dev
machine's trust store, so an honest run fails TLS validation. `--insecure` bypasses
validation **for diagnosis only** and the probe says so on every run. The certificate
inspector still prints the real chain, key size and `SslPolicyErrors` either way, so you
can see precisely why validation failed rather than guessing.
Note the two hazards are separate: an untrusted root affects every client, whereas the
**RSA 1024-bit** DC keys are rejected only by OpenSSL at its default security level —
i.e. by Debian-based .NET *containers*, not by macOS or Windows hosts.
Before deploying, fix trust properly: install `zimmer-rootca` (+ `zimmer-issueca1`) into
the host/container trust store, or pin the chain via
`LdapOptions.ServerCertificateValidationCallback`. The emitted config block always sets
`AllowInsecure: false` for that reason.
## Not shipped
This is a diagnostic, deliberately outside the `ZB.MOM.WW.Auth` solution and its central
package management: `IsPackable=false`, no CI, no tests. It references the Auth projects
directly rather than the `0.2.0` feed package so it always tracks current source.