Status: v1 — ready to execute
This plan scopes the replacement of FreeBSD’s PAM stack
(FreeBSD-pam + FreeBSD-pam-lib packages
plus /etc/pam.d/* policy) on the
freebsd-launchd-mach image with pure-Apple vendored
source. Trigger: pam_xdg.so session-failure mode
during hostnamed iter 1.
Deeper goal: stop carrying FreeBSD’s PAM modules + policy.
OpenDirectory is intentionally out of scope here — it’s deferred to a separate OpenDirectory port scoping doc for later consideration. The PAM port doesn’t depend on any of that work.
FreeBSD-pam* packages from
pkglist-base.txt. No FreeBSD PAM modules, no
FreeBSD /etc/pam.d/* shipped on the image.pam_xdg and the XDG
runtime-dir convention entirely.login(1) + LoginWindow.app,
sshd, su, sudo,
passwd.Pure Apple vendoring — zero new module code authored.
| Component | Apple repo | Provides |
|---|---|---|
| OpenPAM framework + bundled modules | apple-oss-distributions/OpenPAM (457 KB) |
libpam.so framework (replaces
FreeBSD-pam-lib), plus three modules from
openpam/modules/:
|
| Apple standalone modules | apple-oss-distributions/pam_modules (316 KB) |
The 5 portable ones (the others need OD / SecurityServer
/ SmartCard subsystems we don’t have):
|
Net: 1 framework + 8 modules, all from Apple
upstream. Plus an overlay
/etc/pam.d/{login,su,sshd,sudo,passwd,other}
composing the 8 modules into working policies.
Apple’s upstream pam_unix.c (196 LOC) does
getpwnam(user) → crypt(password, hash)
→ compare. It defers backend selection to NSS via the standard
libc API. Whatever the system’s nsswitch.conf
resolves through is what authenticates. The PAM stack has no
opinion about where users live.
This means the same module handles:
/etc/master.passwd (default nss_files
backend) — works out of the box.nss_gershwin.so module) — transparent;
no PAM-side change needed.Gershwin integration is out of scope for this plan. The gershwin desktop ships its own NSS bridge and auth daemon which downstream of this PAM port will transparently provide the user store. Verifying that integration happens at integration time, not as part of the PAM port itself.
Dropping FreeBSD-pam* removes
/usr/lib/pam/pam_*.so and FreeBSD’s stock
/etc/pam.d/*. Question: does this break the
PAM-consuming binaries shipped in FreeBSD-runtime or
FreeBSD-rescue?
| Source | Binary | libpam linkage | Module loading |
|---|---|---|---|
| FreeBSD-runtime | /usr/bin/login |
dynamic -lpam |
runtime dlopen from /usr/lib/pam/ |
/usr/bin/su |
dynamic | runtime dlopen | |
/usr/bin/passwd (+ chsh,
chfn, chpass hardlinks) |
dynamic | runtime dlopen | |
/usr/sbin/sshd |
dynamic (built --with-pam) |
runtime dlopen | |
/usr/sbin/cron |
dynamic | runtime dlopen | |
/usr/bin/at |
dynamic | runtime dlopen | |
| FreeBSD-rescue | /rescue/login |
static libpam.a embedded |
runtime dlopen — static libpam still
loads .so modules at run-time |
/rescue/su |
static | runtime dlopen | |
/rescue/passwd |
static | runtime dlopen |
Key fact: OpenPAM is the same upstream framework on
FreeBSD and on Apple. The ABI is stable. The
pam_sm_* entry points haven’t changed in years.
So a binary statically-linked with FreeBSD’s libpam happily
loads modules from Apple’s pam_modules, and a binary
dynamically-linked against Apple OpenPAM happily loads modules with
FreeBSD-shape entry points too. No binary rebuild or shim
required.
Each /etc/pam.d/<service> file names modules
by basename. When a service runs, libpam parses the file and
dlopens each named module. If a referenced module
file doesn’t exist, PAM returns PAM_OPEN_ERR /
PAM_MODULE_UNKNOWN and the whole service stack fails.
| FreeBSD-only module | Used in stock pam.d? | What we lose | Mitigation |
|---|---|---|---|
pam_login_access.so |
/etc/pam.d/login (auth + account) | Reading /etc/login.access for
per-user/per-tty allow/deny rules |
Drop the lines from our overlay’s pam.d/login. sshd has its own AllowUsers/DenyUsers. |
pam_lastlog.so |
/etc/pam.d/login (session) | Updating /var/log/lastlog on login;
affects last(1) and
finger(1) |
Drop the line. Apple’s pam_uwtmp
still writes utmp/wtmp; lastlog file stays empty.
Could be patched into pam_uwtmp later. |
pam_securetty.so |
Sometimes /etc/pam.d/login | Reading /etc/ttys “secure”
flag to restrict root tty logins |
Drop the line. sshd uses
PermitRootLogin; console root login
restrictions can use login.conf
classes. |
pam_deny.so / pam_permit.so |
/etc/pam.d/other (catch-all default) | Explicit always-deny / always-permit terminators | Vendored by Apple OpenPAM
(openpam/modules/{pam_deny,pam_permit}/).
No additional work. |
pam_unix.so |
Almost every pam.d service | UNIX password verification | Vendored by Apple OpenPAM (196 LOC
upstream DES, getpwnam+crypt).
Goes through NSS. |
pam_xdg.so |
/etc/pam.d/login (session) | Creating /var/run/xdg/$USER |
Intentionally dropped. Goal #2. |
pam_ssh.so |
/etc/pam.d/sshd in some configs | PAM-managed ssh-agent forwarding | Drop. ssh-agent works directly without PAM glue. |
Provided iter 3 ships a complete overlay
/etc/pam.d/{login,su,sshd,sudo,passwd,cron,other}
set that references only the 8 modules we vendor, the following
continue to work unmodified:
login,
su, passwd (+chsh/chfn/chpass),
sshd, cron, at./rescue/login,
/rescue/su, /rescue/passwd.LoginWindow.app
(calls pam_start("login", ...)),
SudoAskPass (bypasses PAM, hits dshelper
socket directly — unaffected by any PAM change).Functional regressions accepted (all small):
last(1) output incomplete (no lastlog
updates)./etc/login.access ignored./etc/securetty ignored for root restriction
(use sshd config + login.conf classes instead).| Iter | Scope | CI gate | Est. LOC |
|---|---|---|---|
| 1 | Vendor Apple OpenPAM as src/openpam/.
Builds libpam.so + pam_unix.so
+ pam_deny.so + pam_permit.so
as a single source drop. Install libpam.so
into /usr/lib/, the 3 modules into
/usr/lib/pam/. Replace
FreeBSD-pam-lib (lib only; modules still
come from FreeBSD-pam at this point so we can verify
ABI compatibility). |
existing post-login markers stay green; new
PAM-FRAMEWORK-OK verifies framework
dlopen of one of our new modules
succeeds |
vendor ~10k, shim ~200 |
| 2 | Vendor 5 standalone Apple modules from
pam_modules
(pam_self, pam_rootok,
pam_uwtmp, pam_nologin,
pam_env) into
src/pam_modules/<name>/; install as
/usr/lib/pam/<name>.so. No policy
change yet. |
new PAM-MODULES-OK: each .so loadable
via dlopen + has required entry points |
vendor ~3k, shim ~100 |
| 3 | Compose overlay
/etc/pam.d/{login,su,sshd,sudo,passwd,other}
using only Apple modules (8 total — 3 from
OpenPAM + 5 from pam_modules). Drop
FreeBSD-pam from
pkglist-base.txt. CI’s existing
post-login marker chain (and a fresh
PAM-LOGIN-OK marker via
su root -c true) is the regression gate. |
existing markers green via the new stack +
PAM-LOGIN-OK |
~50 (configs only) |
| 4 | Restore RunAtLoad=true on hostnamed +
syslogd plists (no pam_xdg means no race
on /var/run/xdg). Verify boot banner shows synthesized
hostname instead of Amnesiac. |
boot banner regex on first login | ~10 (plist edits) |
pam_uwtmp utmp ABI:
Apple’s utmp/wtmp format may differ from FreeBSD’s.
Investigate in iter 2 — if incompatible, either patch
pam_uwtmp to write FreeBSD utmpx, or accept
that last(1) / who(1) output stays
empty (low priority).passwd: line so PAM
consults the expected user store. Iter 3 verifies.UsePAM: confirm OpenSSH
ships with UsePAM yes. If not, overlay
sshd_config.passwd(1) integration:
FreeBSD’s passwd(1) may bypass PAM and
edit /etc/master.passwd directly via
pw_* APIs. If so, dropping
FreeBSD-pam’s passwd entry has no effect
on actual password changes. Test in iter 3.v1 generated 2026-05-25. PAM port is independent of all directory-services decisions; those live in the companion OD port scoping doc.