Skip to content
Open
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
301 changes: 301 additions & 0 deletions harden-host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
#!/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

# 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/<uid>): 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] $*"; }
run() {
if [[ "$DRY_RUN" -eq 1 || "$AUDIT_ONLY" -eq 1 ]]; then
echo "$LOG_PREFIX WOULD RUN: $*"
else
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

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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ---------------------------------------------------------------------------
# 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"

# 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
# ---------------------------------------------------------------------------
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 "==========================================================="