Files
scadaproj/tools/LdapProbe
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
..

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: 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

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.