Technical history · Apple directory services

Before opendirectoryd: NetInfo, DirectoryService & the Open Directory era

How network users, network applications, and single sign-on actually worked on NeXTSTEP and Mac OS X from 1989 to 2011 — the databases, the daemons, the wire protocols, the exact commands, and why Apple threw it all out for a closed daemon in Lion.

Compiled 2026-07-16 for the NextBSD OpenDirectory port scoping · Synthesized from 7 parallel research agents across ~100 primary sources (Apple admin guides, opensource.apple.com drops, NeXT manuals, contemporary O'Reilly texts). Findings attributed inline; full source list at the end.

The short answer

For twenty years Apple ran two generations of directory infrastructure. Era A NetInfo (NeXTSTEP 0.9 → Mac OS X 10.4) was a hierarchical database served over Sun RPC/XDR; a workstation bound upward to a shared parent domain, resolved a user record there, and compared a crypt(3) hash stored right in the record. Homes came over NFS. No Kerberos, no LDAP, no SMB.

Era B Open Directory on Mac OS X Server (10.2 → 10.6) replaced that with an OpenLDAP directory + an Apple Password Server (SASL, multi-hash) + an MIT Kerberos KDC, all fronted by the DirectoryService daemon and its C plugin API. A network user's record carried an AuthenticationAuthority attribute that told login which back end verified the password. Homes came over AFP or NFS; Windows clients hit the same accounts over SMB via Samba + Password Server NTLM hashes.

Yes, there was a real concept of network applications — the /Network/Applications domain and NeXT's /LocalApps, both NFS automounts driven by directory mounts records — but on the Mac it was largely stranded; the idea that actually shipped at scale was NetBoot (whole-system network boot). Era C In 10.7 Lion (2011) Apple replaced the open-source DirectoryService daemon with the closed-source opendirectoryd — XPC out-of-process modules over launchd + libdispatch — mainly to stop third-party plugins from crashing the whole daemon.

1Timeline at a glance

NetInfo debuts — NeXTSTEP 0.9, 1989

NeXT ships NetInfo, replacing BSD flat files and Sun NIS/YP. Sun-RPC program 200100000; local/network/root domain tree; crypt hashes live in the directory.

Mac OS X 10.0–10.1 — 2001

Apple inherits NetInfo as the local and network store; introduces the DirectoryService daemon + plugin API. lookupd brokers libc calls.

Open Directory 1 — 10.2 Jaguar — 2002

Still NetInfo-backed, plus "Authentication Manager" for SMB/CRAM hashes and early Kerberos.

Open Directory 2 — 10.3 Panther — 2003

The pivot. Network directory moves to OpenLDAP (LDAPv3). SASL Password Server replaces Authentication Manager. MIT Kerberos KDC + single sign-on. dscl appears; native Active Directory plugin.

Open Directory 3 — 10.4 Tiger — 2005

Trusted binding, Portable Home Directories (mobile accounts), memberd for UUID ACLs. HTTP NetBoot.

Open Directory 4 — 10.5 Leopard — 2007

NetInfo removed entirely. Local accounts become dslocal XML plists. lookupd folded into DirectoryService; dscacheutil replaces it. autofs replaces the old automounter. Local KDC (LKDC).

10.6 Snow Leopard — 2009

New OpenDirectory.framework (ODSession/ODNode/ODRecord) ships as the modern client API — but still backed by the same DirectoryService daemon. Old C framework deprecated. memberd absorbed.

10.7 Lion — 2011

The break. DirectoryService daemon replaced by closed-source opendirectoryd — XPC modules, launchd, libdispatch. Last complete open drop: DirectoryService-621.12 (10.6.8); the 10.7 tag 655 is a gutted shell. MCX → Profile Manager.

The long decay — 2018–2022

Jan 2018: macOS Server gutted to just Open Directory + Profile Manager. Apr 21 2022: macOS Server app discontinued. opendirectoryd remains the live client daemon today.


2NetInfo: NeXT's distributed directory

NeXTSTEP 0.9 (1989) through Mac OS X 10.4 (2005)

NetInfo — "Copyright © 1989 by NeXT, Inc." in the protocol headers — was a hierarchy of directories (nodes), each holding properties (a key with one or more values). The user store lived under /users; other standard directories were /groups, /machines, /mounts, /printers, /aliases, /exports. A property named _writers_passwd listed who could change a given user's hash — the entire ACL model.

The domain tree, tags, and binding

Domains form a tree. A machine's local domain (tag local) binds upward to a parent/shared domain (conventionally tag network), which can bind to a root domain. A tag names the on-disk <tag>.nidb database, so a parent is addressed as (IP address, tag). Topology was encoded in a machine record's serves property (e.g. server1/network). Three ways a child found its parent:

The daemons

DaemonRole
nibinddThe NetInfo binder. On boot it scans /var/db/netinfo for *.nidb databases and forks one netinfod per database. Answers parent-lookup requests. RPC program 200100001.
netinfodOne instance per served domain; owns the database, answers all data RPCs. A local-tagged domain auto-binds to its parent; other tags do not.
lookupdThe client-side mediator. libc calls (getpwnam, gethostbyname…) never spoke NetInfo directly — they went to lookupd, which multiplexed pluggable agents: CacheAgent, NIAgent (NetInfo), DNSAgent, FFAgent (flat files), LDAPAgent, YPAgent (NIS), DSAgent.

The wire protocol

Both protocols are ONC/Sun RPC over XDR on UDP or TCP, registered with the portmapper (TCP/UDP 111). NeXT reserved RPC program range 200100000–200199999:

Replication & security

A shared domain has one writable master and zero-or-more read-only clones; the master's notification thread pushes each write to clones "usually within seconds," with a checksum-driven full _NI_READALL resync on divergence.

Security posture RPC traffic — including reads that returned the encrypted passwd hash — was cleartext. Access was gated only by the optional trusted_networks property and the _writers_* ACLs. This weakness is a large part of why Apple eventually moved passwords out of the directory entirely (into the Password Server and KDC).

Source availability (verified 2026-07-16)

A full, buildable NetInfo can be assembled from open source — but from two repos at two eras, on APSL 1.x (older than DirectoryService's 2.0):

  • Servers + all tools + lookupdapple-oss-distributions/netinfo at netinfo-369.6 (10.4/Tiger, 2006-03-20) — the last full tree (servers/{netinfod,nibindd,lookupd,notifyd}, tools/{nicl,niutil,nidump,niload,nidomain,nireport,nifind,nigrep}, common/, agents/, resolver/). Do not use 382/384 — those are common/-only shared-library stubs.
  • Client lib libnetinfo (ni_glue.c, ni_util.c, ni_pwdomain.c, sys_interfaces.c) → Libinfo at Libinfo-222.4.12 (≈10.2). Libinfo gutted netinfo.subproj/ to a stub by Libinfo-278 and deleted it by 449.1.3 (10.5).
  • The RPC interface (ni_prot.x / nibind_prot.x, prog 200100000/1) ships in both — inside netinfo/common/ and servers/netinfod/ as well as Libinfo — so you get the XDR even without the client lib.
  • Caveats: no unified LICENSE file (per-file APSL 1.0 in netinfo, APSL 1.1 in Libinfo — audit headers per file); era-skew between 369.6 server structs and 222.4.12 libnetinfo; Project-Builder + rpcgen .x build machinery, not modern tooling.

3How network users worked, end to end

Era A NeXT / NetInfo

An admin created a user in a shared NetInfo domain (root /) rather than the machine-local one. A workstation resolving a non-local user searched its login hierarchy — "starting with the local domain and proceeding toward the root domain until the needed data is located" — and compared the typed password to the record's crypt(3) hash. There was no password server: the hash was the directory field. Home directories lived on NFS and appeared at /Net/hostname/share via the automounter, driven by /mounts records with a net option. lookupd cached everything, invalidating against the NetInfo database checksum.

Era B Mac OS X Server Open Directory

Promoting a server to Open Directory Master (slapconfig -promotereplica) stood up a coordinated trio:

ServiceProcessStorePorts
Account attributesOpenLDAP slapdLDAP DB (RFC 2307 + Apple schema)389 / 636
Password verificationPassword Server PasswordService/var/db/authserver/106 / 3659
Single sign-onMIT krb5kdc + kadmind/var/db/krb5kdc/principal88 / 749

A client was bound via Directory Utility (the LDAPv3 plugin + a spot on the authentication search path; 10.4+ added mutually-authenticated trusted binding). At login, loginwindow asked DirectoryService to find the record, then read its AuthenticationAuthority attribute to decide how to verify — preferring Kerberos (AS-REQ → TGT, enabling SSO to AFP/mail/SMB/VPN), falling back to Password Server SASL for non-Kerberized services. On success it mounted the network home from the record's HomeDirectory URL blob and applied MCX managed preferences.

Extras that defined the era: Portable Home Directories / mobile accounts (;LocalCachedUser;, HomeSync engine — laptops working offline); MCX preferences attached to user/group/computer records; and up to 32 replicas per master (a 10.6 master handled ~200,000 records / 8,192 client connections). The admin GUI was Workgroup Manager.


4Usage: create, list, remove a user

The actual commands, by era — and how a shared/network domain changed them

Era A NeXTSTEP / NetInfo (niutil, niload/nidump)

The domain argument selected the target: . = local machine, / = root of the bound hierarchy (the network/shared domain), and -t host/tag = a specific database not yet wired into a hierarchy.

# Bulk: load a UNIX passwd file straight into a domain
niload passwd . < /etc/passwd          # load flat file into LOCAL domain
nidump passwd .                         # "list": dump domain back out as passwd(5)
nidump passwd . | grep bob              # find a user

# Direct record CRUD with niutil
niutil -create   . /users/bob
niutil -createprop . /users/bob uid 701
niutil -createprop . /users/bob gid 20
niutil -createprop . /users/bob home /Users/bob
niutil -createprop . /users/bob shell /bin/tcsh
niutil -createprop . /users/bob passwd '*'
niutil -read    . /users/bob            # read the record back
niutil -destroy . /users/bob            # remove the user

Local vs. shared domain — what changed: just the domain argument. Apple's own NetInfo guide creates the parent domain by tag because it "is not yet connected to a hierarchy":

nidomain -m myschool
niutil -createprop -t server1/myschool /machines/server1 serves ./myschool
niutil -create -t server1/myschool /users
niutil -create -t server1/myschool /groups

The GUI path was UserManager.app (/NextAdmin): authenticate as root, then in the domain browser "Click '/' (the root domain)… then click OK" to place the account in the shared domain. The 3.3 manual literally warns "it's easy to click OK prematurely."

Era A→B Mac OS X 10.0–10.4 (nicl, and dscl arrives in 10.3)

NetInfo stayed the local store behind DirectoryService. nicl edited it directly; note that modify = delete + recreate:

nicl / -create /users/bob uid 701
nicl / -create /users/bob shell /bin/tcsh
nicl / -create /users/bob home /Users/bob
nicl / -create /users/bob realname "Bob Jones"
passwd bob                              # set the real crypt hash
nicl / -delete /users/bob shell        # "modify" a property
nicl / -delete /users/bob              # delete the user
When dscl appeared dscl shipped with Mac OS X 10.3 Panther (2003), part of Open Directory 2 / the DSTools project — documented side-by-side with nicl/niutil, which were formally deprecated and removed in 10.5 Leopard when the local DB became dslocal XML plists under /var/db/dslocal/nodes/Default/users/.

Era B Open Directory (dscl, dseditgroup, pwpolicy, dsimport)

The node argument now selects the directory: . or /Local/Default for local, /LDAPv3/127.0.0.1 for the server's own network domain.

# Local node CRUD
dscl . -create /Users/bob
dscl . -create /Users/bob UserShell /bin/bash
dscl . -create /Users/bob RealName "Bob Jones"
dscl . -create /Users/bob UniqueID 1001
dscl . -create /Users/bob PrimaryGroupID 20
dscl . -create /Users/bob NFSHomeDirectory /Users/bob
dscl . -passwd  /Users/bob "s3cret"
dscl . -list   /Users                  # list all users
dscl . -read   /Users/bob              # read one record
dscl . -delete /Users/bob              # remove
dscl . -authonly bob s3cret            # test authentication

# Operate on the NETWORK directory — just swap the node
dscl /LDAPv3/127.0.0.1 -list /Users
dseditgroup -o edit -n /LDAPv3/127.0.0.1 -u diradmin -a bob -t user staff

# Password policy, and bulk import (Workgroup Manager shells out to this)
pwpolicy -a diradmin -u bob -setpolicy "minChars=8 maxFailedLoginAttempts=3"
dsimport users.txt /LDAPv3/127.0.0.1 M --username diradmin dirpass

Interactive navigation felt like a filesystem — dscl localhost then cd /LDAPv3/dir.example.com/Users, ls. Password Server was built with mkpassdb; the KDC with kdcsetup / kadmin.local -q "add_principal bob". GUI: Workgroup Manager (accounts) + Server Admin / Directory Utility (services & binding).


5Local user vs. network user, side by side

The discriminator is the AuthenticationAuthority attribute and where the hash actually lives

A local and a network user look almost identical except for three things: the AuthenticationAuthority value, the shape of the home-directory attribute, and — critically — where the password actually lives. A network user's record contains no hash at all.

Local user local node

Hash stored off-record in /var/db/shadow/hash/<GUID> (or crypt-in-record for legacy ;basic;). Home is a plain local path.

AuthenticationAuthority: ;ShadowHash;
GeneratedUID: 6B6A...C2
NFSHomeDirectory: /Users/bob
UniqueID: 1001
PrimaryGroupID: 20
UserShell: /bin/bash
# password hash → /var/db/shadow/hash/6B6A...C2

Network user LDAP node

Two authorities. Hash is not in LDAP — it lives in the Password Server slot DB and as a Kerberos principal. Home is a URL blob.

AuthenticationAuthority:
  ;ApplePasswordServer;0x<slot>,<pubkey> 10.0.1.1:106
  ;Kerberosv5;;bob@REALM;REALM;
HomeDirectory:
  <home_dir><url>afp://files.example.com/Users
  </url><path>bob</path></home_dir>
NFSHomeDirectory: /Network/Servers/files…/bob
# hash → Password Server + KDC, NOT the record
Password typeAuthenticationAuthority valueWhere the secret lives
Legacy crypt;basic; (or attribute absent)In the record's passwd field
Local shadow;ShadowHash; or ;ShadowHash;<methods>/var/db/shadow/hash/<GUID>
Open Directory;ApplePasswordServer;… and ;Kerberosv5;…Password Server slots + KDC principal
Mobile account;LocalCachedUser;Cached locally, synced from server

6Protocols altogether: NFS, LDAP, Kerberos, SMB — which did what

Your direct question — was NFS, Kerberos, SMB, LDAP involved? — has a per-era answer. Here it is stated plainly, then walked through as two concrete logins.

ProtocolNeXT / NetInfoOS X Open Directory
Directory lookupNetInfo Sun-RPC (prog 200100000, port 1033 + portmap 111)LDAPv3 (TCP 389 / 636)
NFS (homes)Yes — automount /Net/host/shareYes — one option for homes (2049) alongside AFP
KerberosNo — never on NeXTYes — KDC on 88, kadmin 749, SSO via GSSAPI (10.3+)
LDAPNo — NetInfo RPC insteadYes — the network directory itself; local accounts never touch it
SMBNoWhere Windows clients hit the same accounts — Samba + Password Server NTLM/LM hashes (445/139)
AFP (homes)Yes — the usual network home transport (TCP 548)
Password verifycrypt(3) compare, in-directoryKerberos AS-REQ, or Password Server SASL on 106/3659

End-to-end: a NeXT network login

loginwindowlookupdNetInfo RPC to the bound parent/root netinfod fetches /users/bobcrypt(3) compare against the record's passwd → home mounted by the NFS automounter from /mounts. Kerberos, LDAP and SMB are not involved.

End-to-end: a bound 10.4/10.5 network login

  1. loginwindowDirectoryService searches the authentication search path.
  2. The LDAPv3 plugin finds /Users/bob over LDAP (389).
  3. DirectoryService reads AuthenticationAuthority and dispatches:
    • ;Kerberosv5;Kerberos AS-REQ to the KDC (88) for a TGT — this is what powers SSO to AFP / mail / SMB / VPN.
    • ;ApplePasswordServer;SASL to Password Server (106 / 3659) for services that can't use Kerberos.
  4. On success, the automounter mounts the home over AFP (548) or NFS (2049) from the HomeDirectory/mounts records; MCX applies.
  5. SMB (445/139) enters when a Windows client authenticates the same OD account — Samba validates against Password Server's SMB-NT/SMB-LM hashes. The Active Directory plugin case swaps the trio for AD's own LDAP + AD Kerberos + DNS SRV + SMB machine-join.
Password Server internals The Password Server exists precisely because different services need different challenge-response schemes that one crypt hash can't satisfy. Per user it stores a PWFileEntry with parallel hash slots: SMB_NT, SMB_LAN_MANAGER, DIGEST_MD5, CRAM_MD5, KERBEROS, SALTED_SHA1, plus recoverable cleartext for APOP/WebDAV-Digest. It publishes a 1024-bit RSA key to secure the SASL exchange (DIGEST-MD5 / CRAM-MD5 / DHX / MS-CHAPv2 / NTLMv1-v2). Its predecessor on 10.0–10.2 was Authentication Manager, holding SMB-NT/LM/CRAM hashes in the NetInfo domain.

One more protocol worth naming: DSProxy — DirectoryService's own protocol over TCP 625 — let Workgroup Manager on an admin box drive a remote DirectoryService daemon. Leopard (10.5) also added the Local KDC (LKDC): every Mac got a per-host realm LKDC:SHA1.<hash> for peer-to-peer Kerberos (AFP, Screen Sharing, Back to My Mac) without a server.


7Network applications & the /Network domain

Yes, the concept existed — and on NeXT it genuinely worked

NeXT had a real network-applications tier. The four-domain model predates the /Network directory: NeXTSTEP used prefixes — ~/Apps (user), /LocalApps + /LocalLibrary (site/network), /NextApps + /NextLibrary (vendor), with a /Net folder for remote filesystems. Workspace Manager's search order (~/Apps → /LocalApps → /NextApps → …) is exactly the user→local→network→system precedence Mac OS X later formalized.

And it was productized: SimpleNetworkStarter made a shared /LocalApps a checkbox — the server exported it, and it "shows up on each computer as /LocalApps," a NetInfo-driven NFS automount with no per-client setup. One copy of Mathematica ran from the server across the whole lab, and fat (multi-architecture) binaries let a single NFS-served .app launch on m68k, i486, SPARC and PA-RISC machines alike.

Mac OS X formalized it — then quietly stranded it

Rhapsody/Mac OS X replaced the prefixes with explicit domain roots, and the Network domain got standard directories: /Network/Applications, /Network/Library, /Network/Servers, /Network/Users, reachable via NSSearchPathForDirectoriesInDomains() with NSNetworkDomainMask (since 10.0). Server-side, Workgroup Manager's Network Mount tab had a literal radio button — "Shared Applications folder" → automount at /Network/Applications (a -static autofs map), alongside "Shared Library folder" and user homes.

What actually happened The home-directory path (/Network/Servers) stayed load-bearing through 10.6 — network login depended on it. But /Network/Applications stayed niche: setup was "about the most convoluted Mac OS X task imaginable" (hand-editing NetInfo mounts/exports), app support was inconsistent, and NFS handled server disappearance badly. Apple never shipped anything into it, Finder network browsing decoupled from it in 10.5, and its admin UI faded with Server Admin/Workgroup Manager after 10.6. The API still returns the path today — pointing at essentially nothing.

The network-application idea that did ship at scale: NetBoot

The real productized "apps from the network" was booting the whole system over the network. NetBoot — demoed by Jobs at Macworld Jan 1999 with a diskless iMac — used BOOTP/DHCP + TFTP + disk images over AFP (later NFS, then HTTP in 10.4). NetBoot 2.0 (10.2+) added Apple's Boot Server Discovery Protocol (BSDP). Diskless clients booted from a read-only image with per-client shadow files absorbing writes, so every restart restored pristine state; NetInstall reused the mechanism to image local disks. It persisted into the macOS Server 5.x era and died with T2/Apple-silicon Macs.

The Mac-era analog of "network apps" was less "serve the binary" and more "control which binaries launch" — MCX Application Launch Restrictions, enforced in 10.5 by the mcxalr agent by code signature. That, too, gave way to configuration profiles / MDM in 10.7.


8The DirectoryService daemon architecture

What "Open Directory" actually named before opendirectoryd — and why it matters for a port

Before Lion, "Open Directory" was a client architecture shipped since 10.0: the DirectoryService.framework access API, the /usr/sbin/DirectoryService daemon, and a C plugin API. Everything was abstracted as nodes/NetInfo/root, /LDAPv3/…, /Active Directory/…, /Local/Default — with a special /Search node implementing the authentication search policy (LSPSearchPath/NSPSearchPath/CSPSearchPath).

The public API (DirServices.h) used opaque handles — tDirReference, tDirNodeReference, tRecordReference — and marshalled all data through tDataBuffer structs, with dsOpenDirService, record CRUD, dsDoDirNodeAuth, and a dsDoPlugInCustomCall passthrough. A notable variant, dsOpenDirServiceProxy, connected to a remote daemon over TCP 625. Record/attribute types carried either a portable dsAttrTypeStandard: prefix or a backend-native dsAttrTypeNative: prefix, with the LDAPv3 plugin's mapping tables translating between them.

Plugins were bundles loaded from the framework's Resources/Plugins (Apple) and /Library/DirectoryServices/PlugIns (third-party), built -bundle_loader /usr/sbin/DirectoryService. The roster evolved: NetInfo → LDAPv3 + native Active Directory + BSD flat-files + the Local plugin (which replaced NetInfo in 10.5, storing dslocal plists). Through 10.4, libc calls went through a separate lookupd; in 10.5 DirectoryService absorbed libinfo resolution and memberd, with dscacheutil/dsmemberutil as the new CLIs.

Why this matters for reuse The entire DirectoryService daemon — daemon core, framework, and most plugins — was Apple Public Source License 2.0 open source. But a source audit (§10) found the last complete drop is DirectoryService-621.12 (10.6.8), whose PlugIns/ holds BSD, Cache, Common, Configure, LDAPv3, Local, Search — not the final 655 tag, where PlugIns/ was stripped back to just Common + Local because opendirectoryd had taken over. A BSD-derived system can legally study and reuse that C/C++ source; opendirectoryd, its replacement, cannot be — it was never open-sourced.
The fatal design flaw Every plugin ran inside the daemon's single heavily-threaded C++ address space, so one bad plugin or corrupt store killed all directory lookups — and since loginwindow authenticated exclusively through it, that meant no logins. Admins reported DirectoryService dying with "Abort trap" and segfaulting "every 10 seconds" on 10.6 servers. Active Directory binding was a perennial sore spot. This is the direct motivation for the opendirectoryd rewrite.

9Why Apple moved to opendirectoryd

The transition came in two steps. 10.6 Snow Leopard introduced OpenDirectory.framework (the ODSession/ODNode/ODQuery/ODRecord class cluster) as a modern client API — but it still talked to the same old DirectoryService daemon underneath, and deprecated the legacy C framework. 10.7 Lion then removed the daemon itself and replaced it with opendirectoryd.

DimensionDirectoryService ≤ 10.6opendirectoryd 10.7+
Launch / concurrencyMach server; explicit thread poollaunchd job; libdispatch/GCD per-request
ExtensionsIn-process C plugins (shared address space)Out-of-process XPC modules (private address space; can't crash the daemon)
Module rolesMonolithic pluginAuthentication / Connection / Discovery / Unspecified
Config location/Library/Preferences/DirectoryService//Library/Preferences/OpenDirectory/Configurations/ + /System/Library/OpenDirectory/
Debug tooling-USR1/-USR2 signals + tail /Library/Logs/DirectoryService/*.logodutil set log debug; odutil show cache|nodes|modules|requests; later unified log
Open sourceAPSL 2.0 (last complete: DirectoryService-621.12)Closed — never released

Motivations analysts cited, in rough priority: fault isolation (XPC modules can't take down the daemon — the single biggest driver given DirectoryService's crash history); security/sandboxing via per-module address spaces; the move to 64-bit + launchd + libdispatch; and code sharing with iOS, which used the OpenDirectory stack without the legacy plugin baggage. Legacy third-party .dsplug plugins survived only through a compatibility shim, dspluginhelperd, before full removal.

Early Lion opendirectoryd was notoriously unstable in AD environments — "network accounts are unavailable," failed binds, 100% CPU — until 10.7.2. The server side then decayed steadily: MCX/Workgroup Manager → Profile Manager; the Jan 2018 gutting of macOS Server down to just Open Directory + Profile Manager; the macOS Server app discontinued April 21, 2022. The client-side opendirectoryd is still the live directory daemon on macOS today.


10Local vs network users — every version

Exact records, storage paths, password location, and home-mount protocol per era (verified 2026-07-16)

§5 gave the concept; this is the exhaustive version. The record shape, the on-disk path, where the password lives, and how the home is mounted all shifted at specific version boundaries — three of them non-obvious (the 10.3 hash externalization, the 10.5 NetInfo→dslocal move, and the 10.7 hash re-embedding). Each era below shows a local account beside a network account.

Era 1 NeXTSTEP 3.x — NetInfo, crypt in-record

Local user — /etc/netinfo/local.nidb

# niutil -read . /users/bob
name: bob
passwd: a89d3Kk1mNP0Q   # DES crypt — IN-RECORD
uid: 501
gid: 20
realname: Bob Example
home: /Users/bob
shell: /bin/csh

Network user — parent domain network.nidb

# same property layout, physically in the
# PARENT domain DB (often on another host,
# reached via local→network→root binding)
name: bob
passwd: a89d3Kk1mNP0Q   # crypt, in the shared record
uid: 501
home: /Net/server/Users/bob
shell: /bin/csh

Password: DES crypt(3) in the record's passwd property — no external file, no separate authority. Home mount: NFS only — a NetInfo /mounts record (name: server:/Users, dir: /Net/server/Users, vfstype: nfs, opts: net) hands the mount to the automounter, landing at /Net/<host>/<share>. NFS 2049 + portmap 111. No AFP client existed for NeXT homes.

Era 2 Mac OS X 10.0–10.4 — NetInfo, the hash walks out

Local user — /var/db/netinfo/local.nidb

# 10.2 and earlier: crypt in-record
authentication_authority: ;basic;
passwd: 3dI880QaIz.Wk   # DES crypt

# 10.3 Panther: hash moves OUT →
authentication_authority: ;ShadowHash;
generateduid: 70902C33-AC79-11DA-…
passwd: ********        # placeholder
# real hash: /var/db/shadow/hash/<GUID>

Network user — shared NetInfo, or LDAP (10.3+)

# home now an XML blob with a URL scheme
home:
 <home_dir><url>afp://server/Users</url>
 <path>bob</path></home_dir>
uid: 1025
# password validated by Password Server,
# not a local hash file

Password boundary: 10.3 Panther externalized the local hash from the record to /var/db/shadow/hash/<GeneratedUID> (a multi-hash blob; 10.4 added salted-SHA1) and switched the authority from ;basic; to ;ShadowHash;. Home mount: AFP default, NFS optional — the afp:///nfs:// <home_dir> blob, mounted by the classic automount(8) daemon (maps -fstab/-static/-nsl) at /Network/Servers/<server>/<share>/<path>. AFP 548, NFS 2049.

Era 3 Mac OS X 10.5–10.6 — dslocal plists + LDAP, autofs

Local user — /var/db/dslocal/nodes/Default/users/bob.plist (short-name keyed)

<key>name</key>        <array><string>bob</string></array>
<key>realname</key>    <array><string>Bob Example</string></array>
<key>uid</key>         <array><string>501</string></array>
<key>home</key>        <array><string>/Users/bob</string></array>
<key>generateduid</key><array><string>70902C33-…</string></array>
<key>authentication_authority</key>
  <array><string>;ShadowHash;</string></array>
# NO ShadowHashData key yet —
# hash STILL external: /var/db/shadow/hash/<GUID>

Network user — LDAP uid=bob,cn=users,dc=…

objectClass: inetOrgPerson posixAccount
             apple-user extensibleObject
uidNumber: 1025
homeDirectory: /Network/Servers/…/bob
apple-generateduid: 8E39B2E0-…
apple-user-homeurl: <home_dir><url>
  afp://server/Homes</url><path>bob</path></home_dir>
authAuthority: ;ApplePasswordServer;0x<slot>,…
authAuthority: ;Kerberosv5;;bob@REALM;REALM;
# LDAP DB on disk: /var/db/openldap/openldap-data/
# hash NOT here → authserver + KDC

Path move: 10.5 Leopard retired NetInfo — local accounts became per-user XML plists under /var/db/dslocal/nodes/Default/users/ (keyed by short name, e.g. bob.plist; siblings groups/ computers/ config/ aliases/). The local hash was still external (/var/db/shadow/hash/<GUID>, salted-SHA1). Network hashes live off-LDAP in Password Server (/var/db/authserver/authservermain) + KDC (/var/db/krb5kdc/principal). Home mount: autofsautomountd (launchd on-demand) triggers under /Network/Servers; the actual mount is performed by NetAuthSysAgent at Login Window using the user's own credentials (homes are unmounted then remounted per-user). Protocol from the URL scheme: AFP 548 default, SMB 445, NFS 2049. autofs doesn't use the user's Kerberos ticket — creds go in the mount map.

Era 4 Mac OS X 10.7+ — opendirectoryd, hash re-embedded

Local user — same bob.plist, hash now inside it

<key>ShadowHashData</key>   # NEW in 10.7 — embedded
<array><data>bplist00…</data></array>

# 10.7 Lion:  SALTED-SHA512
# 10.8+:      SALTED-SHA512-PBKDF2 {
#   entropy = <128-byte derived key>
#   salt    = <32-byte>
#   iterations = ~45000 }
# /var/db/shadow/hash/ no longer used

Network user — LDAP record unchanged

# record shape identical to Era 3;
# hashes still in authserver + KDC.
# What moved is the CLIENT config:
/Library/Preferences/OpenDirectory/
  Configurations/LDAPv3/<server>.plist

apple-user-homeurl scheme migrates:
  afp://…  →  smb://…   # SMB2 default in 10.9

Password boundary: 10.7 Lion pulled the local hash back into the plist as the binary ShadowHashData key (SALTED-SHA512), and 10.8 switched it to SALTED-SHA512-PBKDF2 (entropy/salt/iterations). The external /var/db/shadow/hash/ file is gone. Home mount: transport stack unchanged (autofs + NetAuthSysAgent), but 10.9 Mavericks made SMB2 the default Apple file-sharing protocol, AFP demoted to fallback (later removed entirely ~macOS 15). Mobile/Portable Home accounts are the exception throughout 10.5+: not mounted at all — synced by FileSyncAgent/HomeSync against a local /Users/<name> copy.

Path & storage summary

Era / VersionLocal record pathLocal passwordNetwork record storeNetwork password
NeXT 3.x/etc/netinfo/local.nidbcrypt in-recordnetwork.nidb (parent, poss. remote)crypt in shared record
10.0–10.2/var/db/netinfo/local.nidbcrypt in-record ;basic;shared NetInfo domainshared record / Password Server
10.3–10.4/var/db/netinfo/local.nidbext /var/db/shadow/hash/<GUID> ;ShadowHash;LDAP (10.3+) or shared NetInfoauthserver + KDC
10.5–10.6/var/db/dslocal/…/users/bob.plistext /var/db/shadow/hash/<GUID> (salted-SHA1)LDAP uid=…,cn=users; DB /var/db/openldap/openldap-data//var/db/authserver/ + /var/db/krb5kdc/principal
10.7…/users/bob.plistembedded ShadowHashData = SALTED-SHA512LDAP; config /Library/Preferences/OpenDirectory/authserver + KDC
10.8+…/users/bob.plistembedded ShadowHashData = SALTED-SHA512-PBKDF2LDAP (unchanged)authserver + KDC

Home-mount protocol summary

EraProtocol(s)URL schemeMount toolMount point · ports
NeXT 3.xNFS onlyplain path (NFS in /mounts)BSD automounter/Net/<host>/<share> · 2049, 111
10.0–10.4AFP default, NFSafp:// / nfs://classic automount(8)/Network/Servers/… · 548, 2049
10.5–10.6AFP default, SMB, NFSafp:///smb:///nfs://autofs + NetAuthSysAgent/Network/Servers/… · 548/445/2049/88
10.7+AFP→SMB2 default (10.9)afp://smb://autofs + NetAuthSysAgent (unchanged)/Network/Servers/… · 445, 548, 2049, 88
Mobile / Portable Home (10.5+): not mounted — synced by FileSyncAgent/HomeSync against a local /Users/<name> copy.

11Implementation language by component

Verified from actual source-file extensions in the pinned repos/tags — the C → C++ → Objective-C arc

The language tracks the generation almost perfectly: NetInfo is C, DirectoryService is C++ (behind a C API), OpenDirectory is Objective-C. The password/policy tier is a genuine polyglot. Verified by counting .c/.cpp/.m/.mm in each tree.

Component · layerLanguageEvidence (observed)
NetInfo netinfod, nibinddC + XDRni_prot_proc.c, nibind_main.c, ni_prot.x — 0 .m
NetInfo lookupdObjective-C (+C)15 .m: LUServer.m, DNSAgent.m — the one ObjC piece of the NetInfo era
NetInfo tools (nicl…), common/, libnetinfoC + XDRnicl.c, dsstore.c, ni_glue.c — all C
DirectoryService API frameworkC++ impl, C APIDirServices.cpp (C++) behind the plain-C DirServices.h
DirectoryService daemon core + pluginsC++CServerPlugin.cpp, CHandlers.cpp, CLDAPv3Plugin.cpp, CDSLocalPlugin.cppC* classes
OpenDirectory.frameworkObjective-C + CF CODSession.m/ODNode.m/ODRecord.m and a parallel CFODNode.c/CFOpenDirectory.h C twin
DSPasswordServerPluginPolyglot — C++/ObjC++/ObjC/CCAuthFileBase.cpp, CPSPlugIn.mm, PolicyBase.m, DES.c/SMBAuth.c
DSTools (split by tool)ObjC + some C++dscl.m, dscacheutil.m, DSObjCWrappers are ObjC; pwpolicy is C++
passwordserver_sasl (Cyrus)Cserver.c, checkpw.c, md5.c
opendirectoryd (closed)C++/libdispatch (inferred)UNVERIFIABLE — no source; public reporting only
The developer-facing contrast The two client APIs a consumer actually links against are in different languages: the legacy DirectoryService.framework API (DirServices.h) is plain C; the modern OpenDirectory.framework ships both an Objective-C surface (NSOpenDirectory.h) and a CoreFoundation-C twin (CFOpenDirectory.h) over the same functionality. For a GNUstep-based reimplementation, the OpenDirectory ObjC layer is the natural port target; the C++ DirectoryService daemon is reference for the internals.

One nice historical note: lookupd being Objective-C in ~1991 — with pluggable agent classes (NIAgent, DNSAgent) — is the same out-of-process-plugin idea Apple would eventually rebuild in opendirectoryd two decades later, having detoured through DirectoryService's crash-prone in-process C++ plugins in between.


12What this means for NextBSD

Verified source-availability audit of apple-oss-distributions + the aosm mirror (2026-07-16)

Compiled for the NextBSD OpenDirectory scoping, the reusable-code question was audited directly against the GitHub mirrors rather than taken on trust. The headline correction: the code to port is the 621.12 (10.6.8) generation, not the 655 tag I first cited — by Lion the plugins had already been gutted out of the tree. And "port DirectoryService" is really four separate repos plus one hole.

ComponentLast usable open versionLicenseCompleteness
DirectoryService daemon + plugins621.12 (10.6.8) — not 655APSL 2.0Complete at 621.12: PlugIns/ = BSD, Cache, Common, Configure, LDAPv3, Local, Search. At 655 gutted to Common+Local.
OpenDirectory.framework (ODNode/ODRecord…)146 (10.7)APSL 2.0Complete — real .m impl (ODNode.m, ODQuery.m, ODSession.m), not headers-only.
DSPasswordServerPlugin281.1APSL 2.0Complete — auth-file/policy/replica + SASLCode, DES.c, KerberosInterface.c, SMBAuth.c. Ships as its own repo, not inside DirectoryService.
passwordserver_sasl (SASL lib)215CMU/Cyrus BSD — not APSLComplete — library only.
DSTools (CLIs)162 (10.7)APSL 2.0 (per-file)Partial — has dscl, dscacheutil, dsmbrutil, pwpolicy, dsconfigldap. Missing: dseditgroup, dsimport/dsexport, dsconfigad.
PasswordService daemonAbsent — the actual /usr/sbin daemon storing hash slots in /var/db/authserver was never released.
AD pluginAbsent — always closed.
NetInfo (standalone, legacy)netinfo-369.6 (last full) + Libinfo-222.4.12 (client lib)APSL 1.xComplete but split — servers/tools/lookupd from netinfo-369.6; libnetinfo from Libinfo. Tags 382/384 are stubs. See §2. Cautionary reference, not a port target.
opendirectoryd (10.7+ replacement)Absent / closed — 404 in every mirror; never open-sourced.
Load-bearing conclusions
  • Pin to DirectoryService-621.12. It's the last drop with real plugins. Note that even it lacks the AD plugin (always closed) and the PasswordServer plugin (which lives in its own DSPasswordServerPlugin repo, 281.1) — so a faithful reconstruction pulls from both.
  • You get the client + plugin + policy code, but not the password-server daemon. The PasswordService daemon that fronts /var/db/authserver was never released — that piece you'd write, guided by DSPasswordServerPlugin's slot/hash logic and passwordserver_sasl (which is BSD-licensed, so cleaner to redistribute than the APSL parts).
  • The server trio is mostly upstream already. OpenLDAP slapd + MIT Kerberos KDC + SASL all have FreeBSD-native equivalents; the Apple-specific glue is narrow — apple.schema (confirm it's in 621.12/PlugIns/LDAPv3/), the AuthenticationAuthority dispatch, and the multi-hash slot model.
  • The auth-dispatch abstraction is the idea worth keeping. Storing which back end verifies a password as a per-record attribute (;Kerberosv5; / ;ApplePasswordServer; / ;ShadowHash;) decouples login from any single credential store.
  • The daemon itself: reimplement, don't clone. 621.12's in-process-plugin design is the crash-prone architecture Apple abandoned — mine it for the node model, schema, and API, but rebuild along opendirectoryd's out-of-process lines (which you can imitate but not copy).
  • NetInfo is a cautionary tale, not a target — cleartext RPC, in-directory hashes; Apple spent a decade migrating off it.
  • The file-level execution follows in the companion DirectoryServices porting plan (what to vendor into src, where every file installs, PAM/NSS changes, the directoryservicesd roadmap). It supersedes the earlier deferred scoping doc freebsd-opendirectory-port-plan + its PAM companion — verify against current nextbsd-redux source before acting, per the known doc-staleness caveat.
Hard boundary — one caveat on the audit 10.7 is the last source-available release (DirectoryService-655, OpenDirectory-146, DSTools-162 all date to the Lion cycle); 10.8 is the first fully closed. opendirectoryd returns 404 in both apple-oss-distributions and aosm, and DirectoryService-655's Server/od_passthru.cpp exists only to forward to it — strong corroboration. But: the auditor could not directly read the legacy per-OS lists on opensource.apple.com (now a JS SPA; old paths 404; archive.org was blocked in that environment), so "absent from every Apple release list" rests on the GitHub-mirror evidence, not a read of the original pages. Treat the negative as strongly-supported, not absolutely proven.

13Sources

~100 primary and contemporary sources across 7 research agents. Selected, deduplicated below; man pages cited by version, Apple source by project/tag.