PAM macOS Setup -- Touch ID for sudo
Automated setup for Touch ID authentication on macOS sudo: integrity-verified compilation of pam-reattach, idempotent PAM deployment, and Homebrew distribution via private tap.
PAM macOS Setup -- Touch ID for sudo
A hardened, automated setup for enabling Touch ID authentication on macOS sudo commands. The tool compiles pam-reattach from source with cryptographic integrity verification, deploys it idempotently to /etc/pam.d/sudo_local, and distributes the result via a private Homebrew tap for easy deployment across machines.
The central problem: macOS's native pam_tid.so works for Terminal.app but fails inside VS Code, Cursor, tmux, and other non-native sessions. pam-reattach re-attaches to the Touch ID session, making biometric sudo work everywhere.
See also: co-fita-infrastructure | co-fita-research-operations
1. The Problem
macOS ships with pam_tid.so for Touch ID sudo authentication. However, this module only works in Terminal.app sessions that have direct access to the Biometric framework. In practice, this means:
- VS Code / Cursor integrated terminals -- no Touch ID
- tmux sessions -- no Touch ID
- SSH sessions with agent forwarding -- no Touch ID
- Any non-native terminal emulator -- no Touch ID
The pam-reattach library (by fabianishere) solves this by re-attaching to the macOS Biometric session from any PAM context. The challenge is deploying it correctly: the library must be compiled for the host architecture, placed in the right path, and configured in /etc/pam.d/ without breaking the system.
2. Architecture
The setup has three layers:
setup.sh ← Interactive entry point (AppleScript UI/UX)
│
▼
pam_setup.py ← Autonomous installer (PEP 723, uv run)
│
├── check_integrity() ← SHA256 verification against official release
├── build_pam_reattach() ← Universal binary compilation (arm64 + x86_64)
└── deploy_and_configure() ← Idempotent PAM deployment + cleanup
setup.sh -- Interactive Entry Point
A bash script that provides native macOS UI via osascript dialogs:
- Introduction dialog -- explains what the tool does, warns about admin privileges
- Dependency check -- verifies
uvis installed; offers to install it viaastral.shif missing - Privilege escalation -- detects Touch ID availability via
bioutil -r; either prompts for biometric auth or falls back to an AppleScript password dialog - Delegates to
pam_setup.py-- runs the actual installer viasudo env "PATH=$PATH" pam_setup.py - Success dialog -- confirms Touch ID is now active
The script respects user choice: if the user prefers to run directly in Terminal (avoiding GUI prompts), they can use sudo ./setup.sh instead.
pam_setup.py -- Autonomous Installer
A PEP 723 Python script (run via uv run --script) that handles the full installation lifecycle.
Phase 1 -- Integrity Verification:
EXPECTED_VERSION = "1.3"
EXPECTED_SHA256 = "b1b735fa7832350a23457f7d36feb6ec939e5e1de987b456b6c28f5738216570"
TARBALL_URL = f"https://github.com/fabianishere/pam_reattach/archive/refs/tags/v{EXPECTED_VERSION}.tar.gz"
The script downloads the official release tarball, computes its SHA256, validates against the hash published by the Homebrew Registry, then performs a file-by-file comparison against the locally cloned source in repos/pam_reattach/. This catches both upstream tampering and local modification.
Phase 2 -- Compilation:
clang_cmd = [
"clang",
"-arch", "arm64", "-arch", "x86_64",
"-shared", "-fPIC", "-undefined", "dynamic_lookup",
f"-I{include_path}",
"-o", final_out,
src_pam, src_reattach, "-lpam"
]
Compiles a universal binary (arm64 + x86_64) using Apple Clang. No CMake required -- direct Clang invocation for simplicity and reproducibility.
Phase 3 -- Deployment:
The deployment is idempotent (safe to run multiple times):
- Creates
/usr/local/lib/pam/if it doesn't exist - Copies
pam_reattach.soto/usr/local/lib/pam/pam_reattach.so - Creates
/etc/pam.d/sudo_localwith the correct PAM configuration:
# sudo_local: auth account password session
auth optional /usr/local/lib/pam/pam_reattach.so
auth sufficient pam_tid.so
- Sets ownership to
root:wheelwith permissions0644 - Autocleanup: Audits
/etc/pam.d/sudoand removes any duplicatepam_tid.solines left by previous manual configurations
Why sudo_local instead of modifying sudo directly:
macOS's System Integrity Protection (SIP) protects /etc/pam.d/sudo. The sudo_local file is the Apple-sanctioned extension point -- it is read after sudo and survives macOS updates. This is the correct deployment target.
3. PAM Configuration Details
The two lines that matter
auth optional /usr/local/lib/pam/pam_reattach.so
auth sufficient pam_tid.so
Line 1 (pam_reattach.so, optional):
- Re-attaches to the macOS Biometric session
optionalmeans failure doesn't block authentication -- falls back to password- This is the key enabler for non-native terminals
Line 2 (pam_tid.so, sufficient):
- The native macOS Touch ID module
sufficientmeans if Touch ID succeeds, authentication is complete (no password needed)- If Touch ID fails (e.g., on a Mac without Touch ID), PAM continues to the next module
The cleanup step
The installer also cleans /etc/pam.d/sudo of any pam_tid.so lines that were added by previous manual setups. This prevents conflicts where Touch ID would be checked twice or in the wrong order.
4. Security Properties
| Property | Implementation |
|---|---|
| Source integrity | SHA256 verification against official release tarball + file-by-file comparison |
| No network dependencies at runtime | All downloads happen during installation; the compiled library is self-contained |
| Idempotent deployment | Running the script multiple times produces the same result |
| Autocleanup | Removes duplicate PAM entries from previous configurations |
| Privilege boundary | pam_setup.py only runs with sudo; password is handled via sudo -v (biometric) or sudo -S (cached), never stored |
| No password persistence | The osascript password prompt is a one-time fallback; the password is piped directly to sudo -S and immediately discarded |
| Universal binary | Compiled for both arm64 (Apple Silicon) and x86_64 (Intel) |
Threat model
This tool modifies PAM configuration, which is a sensitive system component. The threat model assumes:
- The user running the script is the machine owner
- The official
pam-reattachrelease v1.3 is trustworthy (verified via hash) - The Homebrew Registry hash is authoritative (pinned in the script)
5. Homebrew Distribution
The pam-mac-setup tool is distributed via a private Homebrew tap at aleffita/homebrew-private.
The Formula
class PamMacSetup < Formula
desc "macOS Sudo Touch ID Configuration Script"
homepage "https://github.com/aleffita/pam-mac-setup"
url "https://github.com/aleffita/pam-mac-setup.git", using: :git, tag: "v1.0.0"
version "1.0.0"
license "MIT"
def install
libexec.install "setup.sh", "pam_setup.py"
(bin/"pam-mac-setup").write <<~EOS
#!/bin/bash
exec "#{libexec}/setup.sh" "$@"
EOS
end
end
Installation on new machines
Option 1 -- PAT token (HTTPS):
brew tap aleffita/private https://github_pat_...@github.com/aleffita/homebrew-private.git
brew install pam-mac-setup
pam-mac-setup
Option 2 -- SSH (cleaner):
brew tap aleffita/private git@github.com:aleffita/homebrew-private.git
brew install pam-mac-setup
pam-mac-setup
The formula is minimal: it installs setup.sh and pam_setup.py into libexec/ and creates a pam-mac-setup wrapper in the Homebrew bin/. The wrapper delegates to setup.sh, which handles the interactive flow.
6. Requirements
- macOS Sonoma or later
- Python 3.11+ (managed via
uv) uvpackage manager (auto-installed if missing)- Apple Command Line Tools (for Clang compilation)
- Touch ID hardware (for biometric auth; password fallback available)
7. Repository Structure
pam-mac-setup/
├── README.md # Documentation (bilingual PT/EN)
├── setup.sh # Interactive entry point (AppleScript UI)
├── pam_setup.py # Autonomous installer (PEP 723)
└── repos/
└── pam_reattach/ # Source code (v1.3, verified, no .git metadata)
The repos/ directory contains the pam-reattach source without git metadata. This is intentional: the integrity verification compares the local source against the official tarball, and .git directories would cause false mismatches.
8. Connection to ClickFix
The ClickFix attack used a fake PAM dialog (Stage 2) to steal the user's password. The dialog used osascript display dialog ... with hidden answer to impersonate a "System Preferences" prompt. This is a social engineering attack that bypasses the real PAM entirely.
The PAM setup tool addresses a different problem (enabling Touch ID for sudo in non-native terminals), but the connection is meaningful: the more commonly Touch ID is used for sudo, the less frequently users type passwords into terminal prompts, and the fewer opportunities attackers have to capture passwords via fake dialogs.
A user who exclusively uses Touch ID for sudo never types their password into a terminal -- making the ClickFix Stage 2 password theft impossible even if the malware reaches that stage.
9. Design Decisions
-
PEP 723 for the Python script --
uv run --scriptmeans zero setup; the user just needsuvinstalled. Dependencies are declared inline in the script header. -
AppleScript for UI -- native macOS dialogs feel trustworthy and integrate with the system. The password prompt uses
with hidden answer(the same primitive the malware abused, but for a legitimate purpose here). -
Integrity verification before compilation -- the script downloads the official tarball and compares it against the local source before compiling. This catches supply chain attacks at the source level.
-
Universal binary -- compiling for both
arm64andx86_64means the same deployment works on both Apple Silicon and Intel Macs. -
sudo_localoversudo-- Apple designedsudo_localas the extension point. Modifyingsudodirectly would be overwritten by macOS updates and could break SIP. -
Autocleanup of old configurations -- users who previously added
pam_tid.soto/etc/pam.d/sudomanually would have conflicts. The installer detects and removes these automatically.