From 99e74167891d5343e41c5f5ededc9cfb0a495d14 Mon Sep 17 00:00:00 2001 From: Berk Date: Tue, 23 Jun 2026 20:36:59 +0000 Subject: [PATCH 1/2] Add harden-host.sh script --- harden-host.sh | 186 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100755 harden-host.sh diff --git a/harden-host.sh b/harden-host.sh new file mode 100755 index 0000000..122f240 --- /dev/null +++ b/harden-host.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +# +# harden-host.sh — Security hardening for the PHYSICAL Mac host that runs Tart +# VMs. Target: macOS Tahoe (26) and later. +# +# Threat model: the host is the durable, attackable surface. It should be a +# trustworthy, compliant endpoint. The build VMs are disposable and relaxed; +# the HOST is where you keep SIP/FileVault/Gatekeeper on and lock down services. +# +# IMPORTANT: This script applies a safe, scriptable SUBSET of CIS Level 1. +# The authoritative path for a fleet is MDM-delivered configuration profiles +# generated by the macOS Security Compliance Project (mSCP), tailored to a +# CIS L1 baseline with SSH deliberately exempted. Use this script for +# standalone hosts or to bootstrap before MDM enrollment. +# +# By default this script AUDITS and applies safe changes. SIP and FileVault +# cannot be safely toggled from a running script (SIP needs recoveryOS; +# FileVault needs interactive en/escrow) — they are verified and reported only. +# +# USAGE +# sudo ./harden-host.sh # audit + apply safe changes +# sudo ./harden-host.sh --audit-only # report posture, change nothing +# sudo ./harden-host.sh --dry-run # print intended changes, do nothing +# +# SSH (Remote Login) is intentionally LEFT ENABLED because CI orchestration +# usually needs it. Lock it down separately (key-only auth, restricted users). +# Pass --disable-ssh to turn it off if your orchestrator does not use it. +# +set -euo pipefail + +DRY_RUN=0 +AUDIT_ONLY=0 +DISABLE_SSH=0 +LOG_PREFIX="[harden-host]" + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=1 ;; + --audit-only) AUDIT_ONLY=1 ;; + --disable-ssh) DISABLE_SSH=1 ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "Unknown argument: $arg" >&2; exit 2 ;; + esac +done + +if [[ "$(id -u)" -ne 0 ]]; then + echo "$LOG_PREFIX must run as root (use sudo)." >&2 + exit 1 +fi + +log() { echo "$LOG_PREFIX $*"; } +ok() { echo "$LOG_PREFIX [ OK ] $*"; } +warn() { echo "$LOG_PREFIX [WARN] $*"; } +run() { + if [[ "$DRY_RUN" -eq 1 || "$AUDIT_ONLY" -eq 1 ]]; then + echo "$LOG_PREFIX WOULD RUN: $*" + else + eval "$@" + fi +} + +FW=/usr/libexec/ApplicationFirewall/socketfilterfw + +echo "===========================================================" +echo " macOS host hardening — $(sw_vers -productName) $(sw_vers -productVersion)" +echo "===========================================================" + +# --------------------------------------------------------------------------- +# SECTION A — Platform protections (verify only; do not toggle from a script) +# --------------------------------------------------------------------------- +log "A. Verifying platform protections (these must stay ON)" + +# SIP +if csrutil status 2>/dev/null | grep -qi "enabled"; then + ok "System Integrity Protection is enabled" +else + warn "SIP is NOT fully enabled. Re-enable from recoveryOS: 'csrutil enable'. A signing host should never run with SIP off." +fi + +# FileVault +if fdesetup status 2>/dev/null | grep -qi "FileVault is On"; then + ok "FileVault is on" +else + warn "FileVault is OFF. Enable + escrow recovery key (ideally via MDM). For unattended reboot use 'fdesetup authrestart'." +fi + +# Gatekeeper +if spctl --status 2>/dev/null | grep -qi "assessments enabled"; then + ok "Gatekeeper assessments are enabled" +else + warn "Gatekeeper is disabled. Attempting to re-enable..." + run "spctl --global-enable 2>/dev/null || spctl --master-enable 2>/dev/null || true" +fi + +# Secure Boot policy (Apple silicon) — report only +if command -v bputil >/dev/null 2>&1; then + log "Secure Boot policy (review for 'Full Security'):" + bputil -d 2>/dev/null | grep -i "security" || true +fi + +[[ "$AUDIT_ONLY" -eq 1 ]] && { log "Audit-only mode: stopping before applying changes."; exit 0; } + +# --------------------------------------------------------------------------- +# SECTION B — Application firewall +# --------------------------------------------------------------------------- +log "B. Enabling application firewall + stealth mode" +run "$FW --setglobalstate on" +run "$FW --setstealthmode on" +run "$FW --setallowsigned on" +run "$FW --setallowsignedapp on" + +# --------------------------------------------------------------------------- +# SECTION C — Disable sharing services (CIS 2.3.x) +# --------------------------------------------------------------------------- +log "C. Disabling sharing services not needed on a build host" + +# Screen Sharing — intentionally LEFT ENABLED for remote administration of the host. +warn "Leaving Screen Sharing ENABLED for remote administration. Restrict it to trusted users/networks." +# Remote Management (ARD) +run "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -deactivate -stop 2>/dev/null || true" +# File Sharing (SMB/AFP) +run "launchctl disable system/com.apple.smbd 2>/dev/null || true" +# Printer Sharing +run "cupsctl --no-share-printers 2>/dev/null || true" +# Remote Apple Events +run "systemsetup -setremoteappleevents off 2>/dev/null || true" +# Internet Sharing +run "defaults write /Library/Preferences/SystemConfiguration/com.apple.nat NAT -dict Enabled -int 0 2>/dev/null || true" +# Bluetooth (no use on a rack host) +run "defaults write /Library/Preferences/com.apple.Bluetooth ControllerPowerState -int 0 2>/dev/null || true" + +# SSH / Remote Login +if [[ "$DISABLE_SSH" -eq 1 ]]; then + log "Disabling Remote Login (SSH) as requested" + run "systemsetup -setremotelogin off 2>/dev/null || true" +else + warn "Leaving Remote Login (SSH) ENABLED for CI access. Harden it: key-only auth, no root login, restricted AllowUsers in /etc/ssh/sshd_config." +fi + +# --------------------------------------------------------------------------- +# SECTION D — Accounts & login window (CIS 2.11.x / 2.12.x) +# --------------------------------------------------------------------------- +log "D. Hardening accounts and login window" +# Disable guest account +run "sysadminctl -guestAccount off 2>/dev/null || true" +run "defaults write /Library/Preferences/com.apple.loginwindow GuestEnabled -bool false" +# Disable guest access to shared folders +run "sysadminctl -smbGuestAccess off 2>/dev/null || true" +# Login window shows name+password fields, not the user list +run "defaults write /Library/Preferences/com.apple.loginwindow SHOWFULLNAME -bool true" +# Disable password hints +run "defaults write /Library/Preferences/com.apple.loginwindow RetriesUntilHint -int 0" +# Require password immediately after screensaver/sleep +run "defaults -currentHost write com.apple.screensaver askForPassword -int 1" +run "defaults -currentHost write com.apple.screensaver askForPasswordDelay -int 0" + +# --------------------------------------------------------------------------- +# SECTION E — Power / energy (CIS 2.10.x) +# --------------------------------------------------------------------------- +log "E. Power settings" +run "pmset -a powernap 0 2>/dev/null || true" # disable Power Nap +run "pmset -a womp 0 2>/dev/null || true" # disable wake for network access + +# --------------------------------------------------------------------------- +# SECTION F — Privacy / analytics (CIS 2.5.x / 2.6.3.x) +# --------------------------------------------------------------------------- +log "F. Disabling analytics sharing and personalized ads" +run "defaults write /Library/Application\\ Support/CrashReporter/DiagnosticMessagesHistory.plist AutoSubmit -bool false" +run "defaults write /Library/Application\\ Support/CrashReporter/DiagnosticMessagesHistory.plist ThirdPartyDataSubmit -bool false" +run "defaults write /Library/Preferences/com.apple.SubmitDiagInfo AutoSubmit -bool false 2>/dev/null || true" + +# --------------------------------------------------------------------------- +# SECTION G — Software update policy +# --------------------------------------------------------------------------- +log "G. Software update policy" +# Keep AUTOMATIC SECURITY responses & XProtect updates ON for the host, but +# control major macOS update timing. Enforce a minimum and defer window via MDM +# in production. Here we ensure security data updates stay enabled: +run "defaults write /Library/Preferences/com.apple.SoftwareUpdate ConfigDataInstall -bool true" +run "defaults write /Library/Preferences/com.apple.SoftwareUpdate CriticalUpdateInstall -bool true" + +echo "===========================================================" +log "Host hardening pass complete." +log "Next: enroll in MDM and push an mSCP-tailored CIS L1 profile for enforced, non-removable settings." +log "Re-run this script (or re-verify Section A) after any macOS update." +echo "===========================================================" From 4a08e1b87a625aee8e78998ea7f873cfd54f3eb5 Mon Sep 17 00:00:00 2001 From: Berk Date: Tue, 23 Jun 2026 20:48:18 +0000 Subject: [PATCH 2/2] Update harden-host.sh: remove power section, add privacy hardening (analytics daemons, ad tracking, Siri/AI agents, telemetry sinkhole) --- harden-host.sh | 129 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/harden-host.sh b/harden-host.sh index 122f240..757fe00 100755 --- a/harden-host.sh +++ b/harden-host.sh @@ -48,6 +48,64 @@ if [[ "$(id -u)" -ne 0 ]]; then exit 1 fi +# The administrator account whose per-user (GUI domain) agents/preferences we +# harden. Defaults to the invoking sudo user; override with ADMIN_USER=name. +ADMIN_USER="${ADMIN_USER:-${SUDO_USER:-$(stat -f%Su /dev/console)}}" +ADMIN_UID="$(id -u "$ADMIN_USER" 2>/dev/null || echo "")" + +# Telemetry / analytics / ad hostnames. ANALYTICS ONLY — safe to sinkhole. We +# deliberately DO NOT touch ocsp.apple.com, timestamp.apple.com, the notary +# service, api.apple-cloudkit.com, developer.apple.com, or +# appstoreconnect.apple.com — those are load-bearing for signing/notarization. +HOSTS_MARK_BEGIN="# >>> appcircle-telemetry-block >>>" +HOSTS_MARK_END="# <<< appcircle-telemetry-block <<<" +TELEMETRY_DOMAINS=( + metrics.apple.com + securemetrics.apple.com + metrics.icloud.com + metrics.mzstatic.com + weather-analytics-events.apple.com + books-analytics-events.apple.com + iadsdk.apple.com + api-adservices.apple.com + supportmetrics.apple.com + xp.apple.com +) + +# System analytics/diagnostics daemons (disabled in system domain). These only +# collect/submit telemetry — none are required for signing, notarization, or +# security updates. +ANALYTICS_DAEMONS=( + com.apple.analyticsd + com.apple.osanalytics.osanalyticshelper + com.apple.SubmitDiagInfo + com.apple.audioanalyticsd + com.apple.wifianalyticsd + com.apple.ecosystemanalyticsd + com.apple.geoanalyticsd +) + +# Per-user agents (disabled in gui/): Siri / Apple Intelligence / phone-home +# suggestion + media-analysis agents. Safe to disable on a build host. +USER_AGENTS=( + com.apple.assistantd + com.apple.Siri.agent + com.apple.siriknowledged + com.apple.assistant_service + com.apple.generativeexperiencesd + com.apple.intelligenceflowd + com.apple.intelligencecontextd + com.apple.intelligenceplatformd + com.apple.knowledge-agent + com.apple.naturallanguaged + com.apple.suggestd + com.apple.parsecd + com.apple.photoanalysisd + com.apple.mediaanalysisd + com.apple.ap.adprivacyd + com.apple.ap.promotedcontentd +) + log() { echo "$LOG_PREFIX $*"; } ok() { echo "$LOG_PREFIX [ OK ] $*"; } warn() { echo "$LOG_PREFIX [WARN] $*"; } @@ -58,6 +116,18 @@ run() { eval "$@" fi } +# Run a command as the admin user inside their GUI launchd domain. +as_user() { + if [[ -z "$ADMIN_UID" ]]; then + warn "No admin user resolved; skipping per-user setting: $*" + return 0 + fi + if [[ "$DRY_RUN" -eq 1 || "$AUDIT_ONLY" -eq 1 ]]; then + echo "$LOG_PREFIX WOULD RUN (as $ADMIN_USER): $*" + else + launchctl asuser "$ADMIN_UID" sudo -u "$ADMIN_USER" "$@" || true + fi +} FW=/usr/libexec/ApplicationFirewall/socketfilterfw @@ -154,13 +224,6 @@ run "defaults write /Library/Preferences/com.apple.loginwindow RetriesUntilHint run "defaults -currentHost write com.apple.screensaver askForPassword -int 1" run "defaults -currentHost write com.apple.screensaver askForPasswordDelay -int 0" -# --------------------------------------------------------------------------- -# SECTION E — Power / energy (CIS 2.10.x) -# --------------------------------------------------------------------------- -log "E. Power settings" -run "pmset -a powernap 0 2>/dev/null || true" # disable Power Nap -run "pmset -a womp 0 2>/dev/null || true" # disable wake for network access - # --------------------------------------------------------------------------- # SECTION F — Privacy / analytics (CIS 2.5.x / 2.6.3.x) # --------------------------------------------------------------------------- @@ -169,6 +232,58 @@ run "defaults write /Library/Application\\ Support/CrashReporter/DiagnosticMessa run "defaults write /Library/Application\\ Support/CrashReporter/DiagnosticMessagesHistory.plist ThirdPartyDataSubmit -bool false" run "defaults write /Library/Preferences/com.apple.SubmitDiagInfo AutoSubmit -bool false 2>/dev/null || true" +# Disable system analytics/diagnostics daemons (effective after reboot). None of +# these are required for signing, notarization, or security updates. +log "Disabling system analytics/diagnostics daemons" +for d in "${ANALYTICS_DAEMONS[@]}"; do + run "launchctl disable system/$d 2>/dev/null || true" +done + +# Limit ad tracking / personalization for the admin user. +log "Disabling personalized ads / limiting ad tracking (user: $ADMIN_USER)" +as_user defaults write com.apple.AdLib allowApplePersonalizedAdvertising -bool false +as_user defaults write com.apple.AdLib forceLimitAdTracking -bool true + +# --------------------------------------------------------------------------- +# SECTION F2 — Siri / Apple Intelligence / phone-home agents +# --------------------------------------------------------------------------- +log "F2. Disabling Siri / Apple Intelligence / suggestion agents (user: $ADMIN_USER)" +if [[ -n "$ADMIN_UID" ]]; then + for a in "${USER_AGENTS[@]}"; do + if [[ "$DRY_RUN" -eq 1 || "$AUDIT_ONLY" -eq 1 ]]; then + echo "$LOG_PREFIX WOULD RUN: launchctl disable gui/$ADMIN_UID/$a" + else + launchctl disable "gui/$ADMIN_UID/$a" 2>/dev/null || true + launchctl bootout "gui/$ADMIN_UID/$a" 2>/dev/null || true + fi + done +else + warn "No admin user resolved; skipping per-user agent disable." +fi + +# --------------------------------------------------------------------------- +# SECTION F3 — Telemetry domain sinkhole via /etc/hosts +# --------------------------------------------------------------------------- +log "F3. Blocking telemetry/analytics domains in /etc/hosts" +if [[ "$DRY_RUN" -eq 1 || "$AUDIT_ONLY" -eq 1 ]]; then + for h in "${TELEMETRY_DOMAINS[@]}"; do echo "$LOG_PREFIX WOULD RUN: block $h"; done +else + # Remove any prior block first (idempotent) + if grep -qF "$HOSTS_MARK_BEGIN" /etc/hosts; then + sed -i '' "/$(printf '%s' "$HOSTS_MARK_BEGIN" | sed 's/[][\.*^$/]/\\&/g')/,/$(printf '%s' "$HOSTS_MARK_END" | sed 's/[][\.*^$/]/\\&/g')/d" /etc/hosts + fi + { + echo "$HOSTS_MARK_BEGIN" + for h in "${TELEMETRY_DOMAINS[@]}"; do + echo "0.0.0.0 $h" + echo "::1 $h" + done + echo "$HOSTS_MARK_END" + } >> /etc/hosts + dscacheutil -flushcache 2>/dev/null || true + killall -HUP mDNSResponder 2>/dev/null || true +fi + # --------------------------------------------------------------------------- # SECTION G — Software update policy # ---------------------------------------------------------------------------