Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .macos
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,40 @@ else
echo "Skipping computer name configuration (COMPUTER_NAME not set)."
fi

# Touch ID for sudo. /etc/pam.d/sudo_local is Apple's own hook for this: it
# survives OS updates where /etc/pam.d/sudo does not, and the template beside
# it ships with the pam_tid line commented out. Created from the template when
# missing, appended to when present without the line, left alone otherwise.
if ! grep -Eqs '^auth[[:space:]]+sufficient[[:space:]]+pam_tid\.so' /etc/pam.d/sudo_local; then
if [[ -f /etc/pam.d/sudo_local ]]; then
printf 'auth sufficient pam_tid.so\n' | sudo tee -a /etc/pam.d/sudo_local >/dev/null
elif [[ -f /etc/pam.d/sudo_local.template ]]; then
sed 's/^#auth/auth/' /etc/pam.d/sudo_local.template | sudo tee /etc/pam.d/sudo_local >/dev/null
else
echo "Skipping Touch ID for sudo (no /etc/pam.d/sudo_local.template on this macOS)."
fi
fi

# One touch for everything. sudo caches a tty-less authorisation per parent
# process, so a tool that runs sudo once per step from fresh processes
# (Ansible's local connection, say) raises a Touch ID prompt for every step —
# measured at ten in one run. A global timestamp makes one touch cover every
# sudo on the machine. The timeout is taste, not part of the fix: `global`
# alone cures the per-process prompts; export SUDO_TIMESTAMP_TIMEOUT (minutes)
# to override the 60 here. The file is validated by visudo BEFORE it is
# installed, at the 0440 sudo expects, so a bad file never reaches sudoers.d.
if ! sudo grep -qs 'timestamp_type=global' /etc/sudoers.d/timestamp \
|| [[ "$(stat -f %Lp /etc/sudoers.d/timestamp 2>/dev/null)" != 440 ]]; then
sudoers_tmp="$(mktemp)"
printf 'Defaults timestamp_type=global\nDefaults timestamp_timeout=%s\n' "${SUDO_TIMESTAMP_TIMEOUT:-60}" > "$sudoers_tmp"
if visudo -cf "$sudoers_tmp" >/dev/null 2>&1; then
sudo install -m 0440 -o root -g wheel "$sudoers_tmp" /etc/sudoers.d/timestamp
else
echo "Skipping the global sudo timestamp: visudo rejected the generated file."
fi
rm -f "$sudoers_tmp"
fi

# Disable the sound effects on boot (Apple Silicon)
sudo nvram StartupMute=%01

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ VMs; the test skips everywhere else unless `MACOS_APPLY_OK=1` is set.
- **`.chezmoiignore`** — target paths chezmoi must not manage (repo-level files everywhere, `CLAUDE.md` and `com.googlecode.iterm2.plist` among them; macOS GUI config off-mac; identity/secrets on `ephemeral`)
- **`bootstrap.sh`** — deprecated wrapper around `chezmoi init --source . --apply`
- **`docs/watchlist.md`** — tools evaluated and parked as "not yet", each with what would make it worth another look; check it before re-evaluating something, and add to it when an evaluation ends in "not yet"
- **`.macos`** — macOS `defaults write` settings; reads `$COMPUTER_NAME` env var for machine-specific naming
- **`.macos`** — macOS `defaults write` settings; reads `$COMPUTER_NAME` env var for machine-specific naming; also enables Touch ID for sudo (via `/etc/pam.d/sudo_local`, Apple's update-proof hook) and a global sudo timestamp (`/etc/sudoers.d/timestamp`, `visudo`-checked before install, mode 0440), because a per-process cache plus Touch ID means one prompt per step for anything that runs sudo from fresh processes; the 60-minute timeout is a preference, overridable with `$SUDO_TIMESTAMP_TIMEOUT`
- **`Brewfile`** — Homebrew formulae, casks, and Mac App Store apps (macOS only; `.chezmoiscripts/darwin/` runs `brew bundle` when it changes)
- **`packages-apt.txt`** — Debian/Ubuntu/WSL package list (`.chezmoiscripts/linux/` installs it when it changes; skips gracefully without apt or sudo)
- **`packages-apt-wsl.txt`** — WSL-only additions, installed on top of the shared list when the machine class is `wsl`
Expand Down
18 changes: 18 additions & 0 deletions tests/macos-apply.bats
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ setup() {
@test "highlight color is set" {
[ "$(defaults read NSGlobalDomain AppleHighlightColor)" = "0.764700 0.976500 0.568600" ]
}

@test "Touch ID for sudo is enabled in sudo_local" {
grep -Eq '^auth[[:space:]]+sufficient[[:space:]]+pam_tid\.so' /etc/pam.d/sudo_local
}

@test "the global sudo timestamp is in effect and its file parses" {
sudo -n visudo -cf /etc/sudoers.d/timestamp
sudo -n -l | grep -q 'timestamp_type=global'
}

@test ".macos is idempotent for the sudo settings" {
cd "$BATS_TEST_DIRNAME/.."
./.macos > "$BATS_TEST_TMPDIR/macos2.log" 2>&1
# Anchored: the template's commented-out line also says pam_tid.so.
[ "$(grep -Ec '^auth[[:space:]]+sufficient[[:space:]]+pam_tid\.so' /etc/pam.d/sudo_local)" = 1 ]
[ "$(sudo -n grep -c 'timestamp_type' /etc/sudoers.d/timestamp)" = 1 ]
[ "$(stat -f %Lp /etc/sudoers.d/timestamp)" = 440 ]
}
17 changes: 17 additions & 0 deletions tests/macos.bats
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@
run grep -q 'AppleHighlightColor -string "0.764700 0.976500 0.568600"' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -eq 0 ]
}

@test "macos enables Touch ID for sudo through sudo_local, never /etc/pam.d/sudo" {
run grep -q 'pam_tid.so' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -eq 0 ]
run grep -E 'tee.*/etc/pam\.d/sudo([^_]|$)' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -ne 0 ]
}

@test "macos makes one sudo authorisation cover every process, and validates the sudoers file" {
run grep -q 'Defaults timestamp_type=global' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -eq 0 ]
# Validated before it is installed, and installed at the mode sudo wants.
run grep -q 'visudo -cf "$sudoers_tmp"' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -eq 0 ]
run grep -q 'install -m 0440 -o root -g wheel "$sudoers_tmp" /etc/sudoers.d/timestamp' "$BATS_TEST_DIRNAME/../.macos"
[ "$status" -eq 0 ]
}
Loading