harden-guest.sh – macOS Build VM Hardening Script – Technical Design Document¶
Introduction¶
Purpose:
This document describes harden-guest.sh, the script that hardens and debloats the ephemeral macOS build VM (the Tart guest) used by the Appcircle self-hosted runner. It records what the script changes, why each change is safe, and how to operate it.
Scope:
The guest VM only: the disposable macOS instance that runs Xcode, code signing, and notarization for a single build. The physical Mac host that runs Tart is hardened separately by harden-host.sh and is out of scope here.
System Overview¶
harden-guest.sh lowers idle CPU, RAM, and outbound network traffic on a build VM while keeping the iOS/macOS build toolchain fully functional. It is meant to be baked into the Tart base image (vanilla to base) and re-run after every macOS point upgrade, because a major or point update resets the launchd override database.
The script applies a curated, conservative set of changes. It targets macOS Tahoe (26) and later on Apple silicon.
Three design principles govern every change:
- System Integrity Protection (SIP) stays enabled. The script uses only
launchctl disable,mdutil,defaults,tmutil,softwareupdate, and/etc/hosts. It never runsbootout/unloadagainst sealed system daemons (blocked by SIP on Tahoe) and never disables SIP. - Idempotent. Safe to run repeatedly.
- Reversible.
--restoreundoes every change the script makes.
Info
The script supports macOS Tahoe (26) and later on the arm64 architecture.
Architecture¶
Technology Stack:
| Layer | Technology |
|---|---|
| Language | Bash (set -euo pipefail) |
| Service control | launchctl (system and per-user GUI domains) |
| Indexing / backup | mdutil, tmutil |
| Preferences | defaults |
| Update control | softwareupdate |
| DNS sinkhole | /etc/hosts |
Execution model:
- Runs as root via
sudo. It exits if the effective UID is not 0. - Per-user agents are changed in the runner user's GUI launchd domain. The target user is
RUNNER_USER(defaults toSUDO_USER, thenadmin);as_userwrapslaunchctl asuser. - Two modes besides apply:
--dry-runprints intended changes without applying them, and--restorereverts.
Key Components¶
Per-user agent list (USER_AGENTS)¶
- Purpose: Disable GUI-session agents that phone home or burn CPU on a headless runner.
- Contents: Siri / Apple Intelligence (
assistantd,Siri.agent,generativeexperiencesd, and related), Spotlight/Siri suggestions (suggestd,parsecd), media analysis (photoanalysisd,mediaanalysisd), iCloud (cloudd,bird), continuity/sharing/games/crash dialogs, and ad personalization. - Action: Disabled in
gui/<uid>and booted out if currently running (user agents are not SIP-sealed).
System daemon list (SYSTEM_DAEMONS)¶
- Purpose: Disable analytics, diagnostics, and push daemons in the system domain.
- Contents:
analyticsd,osanalytics.osanalyticshelper,SubmitDiagInfo, audio/wifi/ecosystem/geo analytics,diagnosticspushd,ReportCrash.Root, andapsd(Apple Push, a persistent outbound TLS connection). - Action: Disabled in the system domain; takes effect after reboot.
Telemetry domain list (TELEMETRY_DOMAINS)¶
- Purpose: Sinkhole analytics and ad endpoints at the DNS layer.
- Contents:
metrics.apple.com,securemetrics.apple.com,metrics.icloud.com, ad services, and similar analytics hosts. - Action: Mapped to
0.0.0.0and::1inside a marked block in/etc/hosts, then the DNS cache is flushed.
Warning
The script deliberately does 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 code signing and notarization. It also never disables trustd, securityd, syspolicyd, accountsd, softwareupdated, or XprotectService.
Detailed Design¶
The apply path performs these steps in order:
- Spotlight indexing off (
mdutil -a -i off, then-Eto erase the index). The largest idle CPU/IO win on a build VM. - Time Machine off (
tmutil disable) and stop new-disk prompts. - Per-user background agents disabled in the runner's GUI domain (and booted out if running).
- System analytics/diagnostics daemons disabled (effective after reboot).
- Diagnostics and crash submission disabled; crash dialogs set to
none. - Automatic software-update scanning off to pin the toolchain (
AutomaticCheckEnabled,AutomaticDownload,AutomaticallyInstallMacOSUpdates, commerceAutoUpdate,softwareupdate --schedule off). - Captive-portal probe off.
- Ad tracking / personalization limited (
com.apple.AdLib). - CI performance conventions matching common runner-image practice: no sleep (
systemsetup -setsleep Off,pmsetsleep/disksleep/displaysleep 0), Power Nap off, screensaver idle 0, App Nap disabled globally (prevents throttling of background build steps), no.DS_Storeon network/USB volumes, and trimmed UI animations. - Telemetry domains sinkholed in
/etc/hosts(idempotent: a prior block is stripped first), then DNS cache flushed.
Restore path: re-enables Spotlight and Time Machine, re-enables every listed user agent and system daemon, restores software-update scheduling and captive-portal detection, and strips the telemetry block from /etc/hosts.
Security Considerations¶
- SIP, Gatekeeper, and the signing/notarization trust chain are never weakened. The script only reduces telemetry and idle background work.
- The
/etc/hostssinkhole is analytics-only; no security or signing endpoint is blocked. - Changes apply to a disposable VM. The durable, attackable surface is the host, hardened by
harden-host.sh.
Usage¶
Run as root on the build VM:
# Apply
sudo ./harden-guest.sh
# Preview only — print what would change, change nothing
sudo ./harden-guest.sh --dry-run
# Revert everything this script changed
sudo ./harden-guest.sh --restore
If the runner user differs from the sudo invoker, set it explicitly:
sudo RUNNER_USER=appcircle ./harden-guest.sh
When to run:
- Once while building the Tart base image (vanilla to base).
- Again after every macOS point upgrade, because the launchd override database is reset by major and point updates.
Danger
Reboot the VM (or re-clone the base image) after running, so the launchd changes take full effect.
Testing and Quality Assurance¶
Verify the toolchain after a reboot:
- A clean Xcode build succeeds.
codesign --timestampsigns without error.notarytool submitreaches the notary service and returns a status.- A second run is idempotent (no duplicate
/etc/hostsblock), and--restorereturns the VM to default behavior.
Risks and Mitigation Strategies¶
| Risk | Mitigation |
|---|---|
| A macOS update re-enables disabled services. | Re-run the script after every point upgrade; it is idempotent. |
| Sinkholing a wrong domain breaks signing/notarization. | The domain list is analytics-only and the signing/notary endpoints are explicitly excluded; the smoke test in Testing catches regressions. |
| A future macOS release seals a currently user-domain agent. | The script tolerates failures per entry (|| true) and is reviewed against each new macOS major version. |
| Disabling software-update scanning hides a critical patch. | The host and the base-image refresh cadence own patching; the guest is disposable and pinned on purpose. |
Appendix¶
Source script: harden-guest.sh (maintained alongside harden-host.sh). Companion documents: the host hardening script doc and the host-vs-guest comparative analysis (Linear PL-240/PL-241/PL-242).