Technical history · Apple directory services
opendirectoryd: NetInfo, DirectoryService & the Open Directory eraHow 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.
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.
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.
Apple inherits NetInfo as the local and network store; introduces the DirectoryService daemon + plugin API. lookupd brokers libc calls.
Still NetInfo-backed, plus "Authentication Manager" for SMB/CRAM hashes and early Kerberos.
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.
Trusted binding, Portable Home Directories (mobile accounts), memberd for UUID ACLs. HTTP NetBoot.
NetInfo removed entirely. Local accounts become dslocal XML plists. lookupd folded into DirectoryService; dscacheutil replaces it. autofs replaces the old automounter. Local KDC (LKDC).
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.
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.
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.
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.
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:
network answers. No reply → fall back to local domain only.| Daemon | Role |
|---|---|
nibindd | The 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. |
netinfod | One instance per served domain; owns the database, answers all data RPCs. A local-tagged domain auto-binds to its parent; other tags do not. |
lookupd | The 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. |
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:
NI_PROG = 200100000 v2 — the data protocol, 29 procedures 0–28: _NI_LOOKUP(10), _NI_LIST(11), _NI_READPROP(14), _NI_RPARENT(22) (find remote parent), _NI_READALL(25), etc.NIBIND_PROG = 200100001 v1 — the binder: NIBIND_REGISTER, NIBIND_CREATECLONE, NIBIND_BIND…local domain, if unconfigured, binds #define LOCAL_PORT 1033 — which is why port 1033 shows up as "netinfo" on a stock Mac.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.
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).
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):
lookupd → apple-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.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).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.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.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.
Promoting a server to Open Directory Master (slapconfig -promotereplica) stood up a coordinated trio:
| Service | Process | Store | Ports |
|---|---|---|---|
| Account attributes | OpenLDAP slapd | LDAP DB (RFC 2307 + Apple schema) | 389 / 636 |
| Password verification | Password Server PasswordService | /var/db/authserver/ | 106 / 3659 |
| Single sign-on | MIT krb5kdc + kadmind | /var/db/krb5kdc/principal | 88 / 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.
The actual commands, by era — and how a shared/network domain changed them
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."
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
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/.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).
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.
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
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 type | AuthenticationAuthority value | Where 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 |
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.
| Protocol | NeXT / NetInfo | OS X Open Directory |
|---|---|---|
| Directory lookup | NetInfo Sun-RPC (prog 200100000, port 1033 + portmap 111) | LDAPv3 (TCP 389 / 636) |
| NFS (homes) | Yes — automount /Net/host/share | Yes — one option for homes (2049) alongside AFP |
| Kerberos | No — never on NeXT | Yes — KDC on 88, kadmin 749, SSO via GSSAPI (10.3+) |
| LDAP | No — NetInfo RPC instead | Yes — the network directory itself; local accounts never touch it |
| SMB | No | Where 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 verify | crypt(3) compare, in-directory | Kerberos AS-REQ, or Password Server SASL on 106/3659 |
loginwindow → lookupd → NetInfo RPC to the bound parent/root netinfod fetches /users/bob → crypt(3) compare against the record's passwd → home mounted by the NFS automounter from /mounts. Kerberos, LDAP and SMB are not involved.
loginwindow → DirectoryService searches the authentication search path./Users/bob over LDAP (389).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.HomeDirectory/mounts records; MCX applies.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.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.
/Network domainYes, 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.
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.
/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 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.
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.
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.opendirectorydThe 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.
| Dimension | DirectoryService ≤ 10.6 | opendirectoryd 10.7+ |
|---|---|---|
| Launch / concurrency | Mach server; explicit thread pool | launchd job; libdispatch/GCD per-request |
| Extensions | In-process C plugins (shared address space) | Out-of-process XPC modules (private address space; can't crash the daemon) |
| Module roles | Monolithic plugin | Authentication / Connection / Discovery / Unspecified |
| Config location | /Library/Preferences/DirectoryService/ | /Library/Preferences/OpenDirectory/Configurations/ + /System/Library/OpenDirectory/ |
| Debug tooling | -USR1/-USR2 signals + tail /Library/Logs/DirectoryService/*.log | odutil set log debug; odutil show cache|nodes|modules|requests; later unified log |
| Open source | APSL 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.
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.
/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.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.
/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>
# 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.
/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>
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: autofs — automountd (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.
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
# 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.
| Era / Version | Local record path | Local password | Network record store | Network password |
|---|---|---|---|---|
| NeXT 3.x | /etc/netinfo/local.nidb | crypt in-record | network.nidb (parent, poss. remote) | crypt in shared record |
| 10.0–10.2 | /var/db/netinfo/local.nidb | crypt in-record ;basic; | shared NetInfo domain | shared record / Password Server |
| 10.3–10.4 | /var/db/netinfo/local.nidb | ext /var/db/shadow/hash/<GUID> ;ShadowHash; | LDAP (10.3+) or shared NetInfo | authserver + KDC |
| 10.5–10.6 | /var/db/dslocal/…/users/bob.plist | ext /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.plist | embedded ShadowHashData = SALTED-SHA512 | LDAP; config /Library/Preferences/OpenDirectory/ | authserver + KDC |
| 10.8+ | …/users/bob.plist | embedded ShadowHashData = SALTED-SHA512-PBKDF2 | LDAP (unchanged) | authserver + KDC |
| Era | Protocol(s) | URL scheme | Mount tool | Mount point · ports |
|---|---|---|---|---|
| NeXT 3.x | NFS only | plain path (NFS in /mounts) | BSD automounter | /Net/<host>/<share> · 2049, 111 |
| 10.0–10.4 | AFP default, NFS | afp:// / nfs:// | classic automount(8) | /Network/Servers/… · 548, 2049 |
| 10.5–10.6 | AFP default, SMB, NFS | afp:///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. | ||||
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 · layer | Language | Evidence (observed) |
|---|---|---|
NetInfo netinfod, nibindd | C + XDR | ni_prot_proc.c, nibind_main.c, ni_prot.x — 0 .m |
NetInfo lookupd | Objective-C (+C) | 15 .m: LUServer.m, DNSAgent.m — the one ObjC piece of the NetInfo era |
NetInfo tools (nicl…), common/, libnetinfo | C + XDR | nicl.c, dsstore.c, ni_glue.c — all C |
| DirectoryService API framework | C++ impl, C API | DirServices.cpp (C++) behind the plain-C DirServices.h |
| DirectoryService daemon core + plugins | C++ | CServerPlugin.cpp, CHandlers.cpp, CLDAPv3Plugin.cpp, CDSLocalPlugin.cpp — C* classes |
| OpenDirectory.framework | Objective-C + CF C | ODSession.m/ODNode.m/ODRecord.m and a parallel CFODNode.c/CFOpenDirectory.h C twin |
| DSPasswordServerPlugin | Polyglot — C++/ObjC++/ObjC/C | CAuthFileBase.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) | C | server.c, checkpw.c, md5.c |
| opendirectoryd (closed) | C++/libdispatch (inferred) | UNVERIFIABLE — no source; public reporting only |
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.
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.
| Component | Last usable open version | License | Completeness |
|---|---|---|---|
| DirectoryService daemon + plugins | 621.12 (10.6.8) — not 655 | APSL 2.0 | Complete 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.0 | Complete — real .m impl (ODNode.m, ODQuery.m, ODSession.m), not headers-only. |
| DSPasswordServerPlugin | 281.1 | APSL 2.0 | Complete — auth-file/policy/replica + SASLCode, DES.c, KerberosInterface.c, SMBAuth.c. Ships as its own repo, not inside DirectoryService. |
| passwordserver_sasl (SASL lib) | 215 | CMU/Cyrus BSD — not APSL | Complete — 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 daemon | — | — | Absent — the actual /usr/sbin daemon storing hash slots in /var/db/authserver was never released. |
| AD plugin | — | — | Absent — always closed. |
| NetInfo (standalone, legacy) | netinfo-369.6 (last full) + Libinfo-222.4.12 (client lib) | APSL 1.x | Complete 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. |
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.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).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.;Kerberosv5; / ;ApplePasswordServer; / ;ShadowHash;) decouples login from any single credential store.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.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.~100 primary and contemporary sources across 7 research agents. Selected, deduplicated below; man pages cited by version, Apple source by project/tag.