Skip to content

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 runs bootout/unload against sealed system daemons (blocked by SIP on Tahoe) and never disables SIP.
  • Idempotent. Safe to run repeatedly.
  • Reversible. --restore undoes 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 to SUDO_USER, then admin); as_user wraps launchctl asuser.
  • Two modes besides apply: --dry-run prints intended changes without applying them, and --restore reverts.

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, and apsd (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.0 and ::1 inside 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:

  1. Spotlight indexing off (mdutil -a -i off, then -E to erase the index). The largest idle CPU/IO win on a build VM.
  2. Time Machine off (tmutil disable) and stop new-disk prompts.
  3. Per-user background agents disabled in the runner's GUI domain (and booted out if running).
  4. System analytics/diagnostics daemons disabled (effective after reboot).
  5. Diagnostics and crash submission disabled; crash dialogs set to none.
  6. Automatic software-update scanning off to pin the toolchain (AutomaticCheckEnabled, AutomaticDownload, AutomaticallyInstallMacOSUpdates, commerce AutoUpdate, softwareupdate --schedule off).
  7. Captive-portal probe off.
  8. Ad tracking / personalization limited (com.apple.AdLib).
  9. CI performance conventions matching common runner-image practice: no sleep (systemsetup -setsleep Off, pmset sleep/disksleep/displaysleep 0), Power Nap off, screensaver idle 0, App Nap disabled globally (prevents throttling of background build steps), no .DS_Store on network/USB volumes, and trimmed UI animations.
  10. 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/hosts sinkhole 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 --timestamp signs without error.
  • notarytool submit reaches the notary service and returns a status.
  • A second run is idempotent (no duplicate /etc/hosts block), and --restore returns 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).