16 KiB
Zimmer NAM — LDAP / Active Directory Auth Setup
Reference for configuring services to authenticate against the Zimmer NAM domain.
Extracted from the production Ignition 8.1 gateway Ignition-IGNTN-APP-VP01
(config.idb, NAM AD-Hybrid user source), audited 2026-08-05. Full audit:
~/Desktop/config-idb-users-groups.md.
Directory / connection
| Setting | Value |
|---|---|
| Domain | nam.zimmer.com |
| Primary DC | uswap-vdc-nam1:389 |
| Secondary DC | uswap-vdc-nam2:389 |
| Encryption | None — plain LDAP on 389 (no LDAPS/StartTLS in the Ignition config) |
| SASL | DIGEST-MD5, CRAM-MD5 offered; QoP auth-conf,auth-int,auth; strength high,medium,low; no mutual auth |
| Referrals | FOLLOW |
| Read timeout | 60 000 ms |
| Paging | 1000 entries/page |
| Anonymous bind | Disallowed |
Bind identity (UPN, not DN)
- Service account:
svc_igntnapp - Binds by UPN:
svc_igntnapp@nam.zimmer.com— there is no bind DN/CN anywhere in the working config; end users also authenticate as<sAMAccountName>@nam.zimmer.com(automatic suffix). - Password: held encrypted by the gateway; obtain from the credential owner — never
commit it (in this repo's services use a
${secret:NAME}reference).
User lookup
| Setting | Value |
|---|---|
| Search base | domain root DC=nam,DC=zimmer,DC=com (Ignition field left empty) |
| User search filter | (&(objectClass=user)(sAMAccountName={0})) |
| User enumeration filter | (&(objectClass=user)(!(objectClass=computer))) |
| Username attribute | sAMAccountName |
| Display name | name |
mail |
|
| Phone / SMS | telephoneNumber / mobile |
Groups / roles
The working setup does not use LDAP groups at all. The Ignition profile is AD-Hybrid: AD answers who you are, while what you may do (roles Administrator, Designer, Engineer, Maintenance, Management, Mfg_Tech, Operator) is assigned manually per user inside the application. AD security-group membership grants nothing.
Implication for new services: don't assume a memberOf-based authorization
scheme exists to mirror — there is no established group→role convention in this
domain's SCADA stack. Either keep app-local role assignment (matching the
existing pattern) or define new AD groups deliberately; standard AD attributes
(memberOf, filter (&(objectClass=group)(member={0}))) are available if we
choose to.
Superseded in part (2026-08-12). The statement above is correct about Ignition, which was the only source audited. It is wrong as a claim about the domain: a
UG_WW_*Wonderware group convention does exist, already scoped by environment and site — seeUG_WW_*. Read that before choosing an authorization design.
Behavioral notes from the working config
- Users are populated on demand: a local record is created on first successful AD login (with no roles until granted).
- Failover: soft failover to the local internal user source, 60 s credential-cache validation — cached logins keep working through a DC outage.
- Lockout: 5 failed attempts / 15-minute window.
Cautions
- Port 389 without TLS means simple binds transit cleartext unless SASL confidentiality is negotiated — for new .NET services prefer LDAPS (636) or StartTLS against the same DCs.
TLS probe results (verified 2026-08-10, from the dev Mac)
Both DCs were live-probed; LDAPS and StartTLS both work on both DCs.
| Check | uswap-vdc-nam1 | uswap-vdc-nam2 |
|---|---|---|
| Ports open | 389, 636, 3268, 3269 | 389, 636, 3268, 3269 |
| LDAPS (636) handshake | TLSv1.3 OK | TLSv1.3 OK |
| StartTLS on 389 | OK | OK |
| Anonymous rootDSE over LDAPS | result: 0 Success |
result: 0 Success |
| Cert subject/SAN | USWAP-VDC-NAM1.nam.zimmer.com (matches FQDN) |
USWAP-VDC-NAM2.nam.zimmer.com (matches FQDN) |
| Cert validity | 2026-03-21 → 2027-03-21 | 2026-03-20 → 2027-03-20 |
| Chain | leaf ← zimmer-issueca1 ← zimmer-rootca (intermediate served, root not) |
same |
| Key size | RSA 1024-bit (sha256 sig) | RSA 1024-bit (sha256 sig) |
Implications for new .NET services:
- Use LDAPS 636 (or StartTLS) — no reason to accept cleartext binds.
- Trust:
zimmer-rootcais an internal CA, not in default OS/container trust stores. Either install the root (+ intermediate) into the host/container trust store, or pin the chain viaLdapOptions.ServerCertificateValidationCallback(code-only seam inZB.MOM.WW.Auth.Ldap). - ⚠️ RSA 1024-bit DC certs: OpenSSL at default security level (SECLEVEL=2 —
what Debian-based .NET containers ship) rejects 1024-bit keys
(
EE certificate key too weak, observed in this probe), so a Linux/container client may fail the TLS handshake even with the CA trusted. Windows/Schannel hosts accept them. Options: get the DC cert template reissued at 2048+ (right fix, needs AD PKI owner), or lower SECLEVEL / use a custom validation callback in affected containers (workaround). Certs expire 2027-03 — a reissue opportunity. - Hostnames resolve from the dev network: nam1 =
10.201.216.2, nam2 =10.201.138.78.
Probe tool
tools/LdapProbe (ldapprobe) tests these settings interactively and
dumps a user's group membership. It drives the real ZB.MOM.WW.Auth.Ldap library, so its
verdict is what the four apps will produce, and separately queries the directory for the
things the library cannot report — nested groups, the primary group, group types and
referral behaviour.
dotnet run --project tools/LdapProbe -- --insecure --self-bind <sAMAccountName>
dotnet run --project tools/LdapProbe -- --sweep --insecure # connectivity only
Re-verified 2026-08-12 (from the dev Mac, via ldapprobe --sweep)
All six endpoint/transport combinations connect: LDAPS 636, StartTLS 389 and plain 389 on both DCs (~210–235 ms TLS, ~32 ms plaintext).
Two refinements to the TLS notes above, from the actual chain a .NET client sees:
- The only validation error is
UntrustedRoot—SslPolicyErrorsisRemoteCertificateChainErrors, and both the leaf andzimmer-issueca1reportok. So installingzimmer-rootcais sufficient to make LDAPS work without any validation bypass. - .NET on macOS accepts the RSA 1024-bit key. The weak-key rejection is specific to
OpenSSL at
SECLEVEL=2, i.e. Debian-based .NET containers — it is not a cross-platform blocker, and a Windows host is unaffected too. That matters because three of the four apps deploy to Windows onwonder-app-vd03, where this hazard does not apply at all; only containerized deployments need the SECLEVEL workaround or a reissued 2048-bit cert.
Cert observed on nam1: CN=USWAP-VDC-NAM1.nam.zimmer.com, SAN matches, sha256RSA,
valid 2026-03-20 → 2027-03-20, chain leaf ← zimmer-issueca1 ← zimmer-rootca.
UG_WW_* — an existing Wonderware group convention (observed 2026-08-12)
This partly overturns the "Groups / roles" section above. That section is accurate about Ignition — it ignores AD groups and assigns roles per-user. But the Wonderware estate does have a group convention, and it is already environment- and site-scoped.
Full directory inventory, 39 groups — enumerated 2026-08-12 with
ldapprobe -g 'UG_WW_*' against DC=nam,DC=zimmer,DC=com (not one account's membership;
16 of these are held by nobody on the probing account). Every one is a universal security group — exactly the shape ZB.MOM.WW.Auth.Ldap consumes, with no nesting
problem and no distribution-group trap. Member counts are as observed.
Platform, environment-scoped — one set per DEV/QA/PROD:
| Group | Members | Notes |
|---|---|---|
UG_WW_APP_ADMIN_{DEV,QA,PROD} |
7 / 6 / 5 | Application Server admin |
UG_WW_APP_IDE_{DEV,QA,PROD} |
6 / 6 / 4 | ArchestrA IDE — Galaxy authoring |
UG_WW_APP_IDE_RO_{DEV,QA,PROD} |
2 / 1 / 1 | read-only IDE tier |
UG_WW_HIST_ADMIN_{DEV,QA,PROD} |
7 / 6 / 4 | Historian admin |
UG_WW_CITRIX_{DEV,QA} |
6 / 12 | Citrix access; no PROD group exists |
UG_WW_BTMGR_BE_{QA,PROD} |
3 / 5 | Batch Manager, plant BE |
UG_WW_BTMGR_MC_{QA,PROD} |
2 / 5 | Batch Manager, plant MC |
UG_WW_BTMGR_RO_{QA,PROD} |
EMPTY / 4 | read-only Batch Manager |
Site admin — UG_WW_SITE_ADMIN_<SITE>, 6 members each, sites
BER GAL PAR PON SHN WN0A WW05 WW19.
Plant-floor, per site and area — this is the tier with real operator populations:
| Group | Members |
|---|---|
UG_WW_B19_SMS_OPERATOR |
34 |
UG_WW_B19_SMS_TESTER |
13 |
UG_WW_B19_VitE_ENGINEER |
4 |
UG_WW_B19_VitE_OPERATOR |
10 |
UG_WW_PON_PRESS_ENGINEER |
14 |
UG_WW_PON_PRESS_OPERATOR |
21 |
UG_WW_WN_ARCOM_OPERATOR |
5 |
UG_WW_WN_ARCOMXL_OPERATOR |
2 |
UG_WW_WN_SOLAR_OPERATOR |
10 |
UG_WW_WN_STERIS_OPERATOR |
5 |
UG_WW_WN_STERIS_SUPERVISOR |
6 |
Decoded convention — three shapes, and a consistent role suffix:
UG_WW_<FUNCTION>[_RO]_<ENV> APP_ADMIN | APP_IDE | HIST_ADMIN | BTMGR | CITRIX
UG_WW_SITE_ADMIN_<SITE> BER GAL PAR PON SHN WN0A WW05 WW19
UG_WW_<SITE>_<AREA>_<ROLE> sites B19 PON WN; roles below
Role suffixes in use: ADMIN, RO (read-only), OPERATOR, ENGINEER, SUPERVISOR,
TESTER.
Mapping onto the canonical role set
The suffixes line up closely with
CANONICAL-ROLES.md, so
IGroupRoleMapper<CanonicalRole> can map onto this rather than inventing a parallel scheme:
| AD suffix / family | Canonical role | Rationale |
|---|---|---|
_RO_ |
Viewer | OBSERVE only |
_OPERATOR |
Operator | OBSERVE + OPERATE |
_ENGINEER |
Engineer | adds TUNE |
APP_IDE_* |
Designer | the ArchestrA IDE is the authoring surface — AUTHOR |
APP_ADMIN_*, SITE_ADMIN_* |
Administrator | |
HIST_ADMIN_* |
Administrator, historian-scoped | |
_SUPERVISOR, _TESTER |
undecided | see below |
Three gaps to settle before this is a design rather than a sketch:
_SUPERVISORand_TESTERhave no obvious canonical equivalent.SUPERVISORsits aboveOPERATORbut whether it meansTUNE(⇒ Engineer) or approval authority is not derivable from the name.TESTERappears only in QA context. Ask the owners; do not guess.- Nothing maps to
Deployer. The canonical set separatesDEPLOYfromAUTHOR; this directory does not. In practiceAPP_ADMINholds deploy rights, which collapses the separation — the same accepted SoD collapse ScadaBridge already carries. - The environment axis (
DEV/QA/PROD) is orthogonal to role and has no representation inCanonicalRole. It has to ride in the per-project scope payload, not the role — otherwise the role set multiplies by three.
Caveats:
- Group container placement is inconsistent (
OU=Universal,OU=Groups,OU=Warsaw,OU=Groups,OU=Warsaw, andOU=Groupsat the domain root). Harmless formemberOf-based authorization — only the user search base matters — but any group-enumeration search must be based at the domain root. UG_WW_BTMGR_RO_QAis empty. Mapping a role to it grants nobody anything today.- Descriptions are ServiceNow ticket ids (
TASK0638550etc.), so these groups are provisioned by ticket — requesting new ones has an established process, and the ticket id is the audit trail for why a group exists. - No group here is specific to OPC UA, ScadaBridge or this family; they describe the existing Wonderware estate. Reusing them means accepting their semantics.
Library defect: a domain-root search base failed on AD referrals — FIXED in Auth 0.2.1
Found and fixed 2026-08-12. All four Auth packages are published to the Gitea feed at
0.2.1 and restore-verified from a scratch consumer (the assembly restored from the
feed carries the fix; 0.2.0 does not). ADOPTED by all four apps 2026-08-13 —
OtOpcUa master @ 08e33fc7, ScadaBridge main @ d28824d9, MxAccessGateway
main @ 1cb14d2, HistorianGateway e42ff31 (on its dashboard-ui-sweep-theme-0.4.1
branch) — each built + tested green; commits are local, not yet pushed. FallbackServers
failover was live-gated the same day against the local redundant GLAuth pair
(infra/glauth-redundant/): HistorianGateway dashboard login
drill PASS (sticky failover both directions).
Authenticating with SearchBase = DC=nam,DC=zimmer,DC=com fails on both DCs with
ServiceAccountBindFailed in ~546 ms. It is not connectivity, not the service account,
and not failover — both endpoints fail identically at the search stage:
! LDAP endpoint uswap-vdc-nam1.nam.zimmer.com:636 failed system-side at search
! LDAP endpoint uswap-vdc-nam2.nam.zimmer.com:636 failed system-side at search
✗ FAILED — ServiceAccountBindFailed
Cause: AD returns a continuation referral for a search based at the domain root, and
NovellLdapConnection.Search (in ZB.MOM.WW.Auth.Ldap) iterates results.Next() without
catching LdapReferralException. The exception escapes the search stage, and
LdapAuthService maps it to the system-side bucket. The identical search from ldapprobe's
own RawDirectory.Search, which catches and counts referrals, succeeds against the same
base — which is what isolates it to the library rather than the directory.
The fix (0.2.1): NovellLdapConnection.Search now delegates to an internal
ReadEntries(ILdapSearchResults) that catches LdapReferralException — and any
LdapException carrying ResultCode == Referral — inside the result loop and continues.
A continuation reference is a pointer to another naming context, never a statement that
this search failed, so skipping it returns exactly the entries in the naming context we
bound to. A referral counter caps the loop at 1000 as a spin guard, since a client that
threw without advancing would otherwise hang inside a fail-closed auth path.
Non-referral faults still propagate untouched — swallowing those would turn "the directory
is broken" into "no such user", which is the wrong thing to say about someone's
credentials. Covered by NovellSearchReferralTests (7 tests through the
ILdapSearchResults seam, so no live directory is needed; 6 of the 7 verified red against
the pre-fix code, the seventh being the non-referral control).
Until 0.2.1 is adopted, the only workaround is to narrow SearchBase below the
domain root — a landmine, because the base must contain every user who will ever log in
and a user in another OU silently gets UserNotFound. Do not use the container
ldapprobe's auto-retry reports: that is the test user's own OU (e.g.
OU=Office Users,OU=Warsaw,…), not a production value.
This also constrained group enumeration: UG_WW_* groups are spread across three
containers including the domain root, so a full group listing hit the same referral.
ldapprobe's --list-groups carries its own referral-tolerant reader and was never
affected.
The zero-group rule is still the risk
ZB.MOM.WW.Auth.Ldap refuses a zero-group login: a user whose password verifies but
whose memberOf is empty comes back as GroupLookupFailed, not success. The UG_WW_*
finding above makes this less likely for Wonderware-estate staff, but it is not disproven
— the account probed held 176 groups, which is not representative. Test a real operator
or service account before designing around this. Two further traps the probe checks for
explicitly:
- the primary group is invisible. AD omits it from
memberOf, so a user whose only membership isDomain Userscan never satisfy the library's rule. - nested groups are invisible. The library reads
memberOf, which holds direct edges only — mapping a role to a group the users reach through another group would grant nobody anything. The probe resolves the transitive set separately and flags the difference.