MacSecLabs
Phase 00 · Module 0.2 · Beginner

The macOS 27 Trust Map: SIP, Code Signing, Gatekeeper, TCC, and the AI Trust Plane

Every technique you will learn in this course does one of three things. It respects a trust boundary, it abuses a gap in one, or it creates evidence that one was crossed. There is no fourth option. That is why the operator who can name every boundary on the target, say what each one enforces, and predict what each one records is the one who controls the engagement instead of guessing through it.

This module is your map. You will inspect every major trust boundary on your Golden Gate lab Mac, with real commands and real output, and you will meet the AI-related trust boundaries as they appear on this macOS 27 build. Apple Intelligence and privileged assistant services predate this release; the research question is which actors, entitlements, and routes changed here. Later phases assume you can read this map without hesitation. When a persistence module says a LaunchAgent survives reboot, you will know which launchd domain owns that decision. When an execution module says a payload must be ad hoc signed, you will know that AMFI is the control being satisfied. When an agentic module points at Siri AI's entitlement list, you will already know what an entitlement is worth.

One habit from module 0.1 carries forward: everything you inspect gets captured as evidence. A trust map you cannot show to a reviewer is just an opinion.


Establish the Trust Baseline

Continue in the Phase 00 lab workspace prepared in module 0.1. The read-only trust survey and workspace verifier used below are already in that package.


Concept: The Layered Model

macOS does not have one security boundary. It has a stack of them, and each layer is owned by a different subsystem with its own policy store, its own enforcement point, and its own telemetry. Here is the map this module walks, with the question an operator asks of each layer:

LayerWhat it enforcesThe operator question
SIPSystem file immutability, even for rootWhich paths are read-only no matter who I am?
Code signingBinary identity and integrityWho signed this, and has it been touched since?
AMFIRuntime enforcement of signing policyWill the kernel kill this code on load?
Hardened RuntimePer-process restriction flagsWhich dangerous capabilities are denied even to signed code?
GatekeeperFirst-launch assessment of downloaded appsDoes this artifact need user approval to open?
NotarizationApple-issued launch ticketHas Apple scanned and ticketed this binary?
QuarantineDownload provenance markerIs this file treated as coming from the internet?
SandboxProcess containment profileWhat files, network, and IPC can this process reach?
TCCUser privacy policyWhich data requires a grant, prompt, or managed policy?
launchdProcess lifecycle, domains, and privilegeWho can start, stop, and own this service?
MDM and profilesEnterprise policy overlayIs there organizational policy above the local admin?
Unified loggingSystem-wide telemetryWhat did the platform record about all of the above?
AI-related servicesEntitlement-rich assistant and model servicesWhich actors and privileges are present or changed on this build?

Read the table twice. The last row is a live inventory question, not a claim that assistant or model services first appeared in macOS 27. The rest of this module inspects each row on your machine, in roughly this order.


SIP and the Sealed System Volume

Start at the bottom of the stack, because everything else stands on it. System Integrity Protection, SIP, is a kernel-enforced rule that certain system locations are read-only even for root. It is not a permission system you can configure per file. It is a hard fence around the parts of the OS that implement every other control on the map. If an attacker with root could edit those paths, every other boundary would be theater.

SIP status
% export EVIDENCE="${MSL_EVIDENCE_ROOT:-$HOME/macseclabs/evidence}/0.2-trust-boundaries"
% mkdir -p "$EVIDENCE"
% csrutil status | tee "$EVIDENCE/sip-status.txt"
System Integrity Protection status: enabled.

Enabled is the assumed state for this entire course. SIP can only be reduced from Recovery Mode, which requires physical or console access and a reboot, and any reduction is trivially visible to a defender who runs the same command. An operator on a real engagement treats a SIP-disabled host as a finding to report and exploit cautiously, not a condition to create.

SIP's policy fence has a physical counterpart. The operating system is not just protected by rules; it is mounted from a cryptographically sealed volume:

the sealed system volume
% mount | awk '$3 == "/" && $4 ~ /apfs/ { print; found=1 } END { if (!found) exit 1 }' | tee "$EVIDENCE/sealed-volume.txt"
/dev/disk3s1s1 on / (apfs, sealed, local, read-only, journaled)

The device number varies by Mac; identify the root mount by /, not by disk3s1s1. Read the flags. sealed means the volume carries a Merkle tree of its content and the kernel verifies it. read-only means no write path exists at all. This is the Signed System Volume, and it is why the classic move of "drop a binary into /usr/bin" is not just blocked by SIP policy but physically impossible on modern macOS. Your payloads will live in writable places: /tmp, user home paths, and locations you will map in Phase 01.


Code Signing: Identity and Integrity

Code signing answers two questions about every executable on the system: who signed it, and has it changed since signing. Nearly every other boundary on the map uses the answer as input. Gatekeeper checks it. AMFI enforces it. TCC identifies apps by it. If you do not know the signing state of your own tooling, you cannot predict how any gate will treat it.

signature of a platform binary
% codesign -dv /bin/zsh 2>&1
Executable=/bin/zsh
Identifier=com.apple.zsh
Format=Mach-O universal (x86_64 arm64e arm64e.x1)
CodeDirectory v=20400 size=1478 flags=0x0(none) hashes=41+2 location=embedded
Platform identifier=26
Signature size=4202
Signed Time=Aug 8, 2026 at 4:00:07 PM
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements count=1 size=64

Four details in this output matter, and learning to read them is a permanent skill for this course.

Format=Mach-O universal (x86_64 arm64e arm64e.x1) shows an x86_64 slice and two arm64e variants. Golden Gate is an Apple Silicon-only OS, yet zsh still ships an x86_64 slice. This is the last release with general-purpose Rosetta 2 support, and universal platform binaries are part of that wind-down. arm64e is the pointer-authenticated ARM64 variant used by platform code.

Platform identifier=26 identifies a platform-signed binary; TeamIdentifier=not set means this display does not name a developer team. Those fields alone do not establish the volume seal. Verify the binary's path and the volume posture separately before making that claim.

CodeDirectory is the integrity mechanism: a hash over every page of the binary. Change one byte and the code directory no longer matches.

Build a tiny script and ask codesign what it sees before any signing:

an unsigned script
% SIGN_TEST=$(mktemp "${TMPDIR:-/tmp}/msl-0.2-sign.XXXXXX") || return 1
% printf '#!/bin/sh\necho hello from the lab\n' > "$SIGN_TEST"
% chmod +x "$SIGN_TEST"
% codesign -dv "$SIGN_TEST" 2>&1
% test "$?" -eq 1
/var/folders/mp/bsl2gzwj4n73flkxdn8bphg00000gn/T//msl-0.2-sign.cij7C0: code object is not signed at all

Now sign it ad hoc and read the signature again:

ad hoc signing changes the verdict
% test -n "$SIGN_TEST" && test -f "$SIGN_TEST" && test ! -L "$SIGN_TEST" || return 1
% codesign --force --sign - "$SIGN_TEST" 2>&1
% codesign -dv "$SIGN_TEST" 2>&1
% rm -- "$SIGN_TEST"
Executable=/private/var/folders/mp/bsl2gzwj4n73flkxdn8bphg00000gn/T/msl-0.2-sign.cij7C0
Identifier=msl-0.2-sign
Format=generic
CodeDirectory v=20100 size=157 flags=0x2(adhoc) hashes=1+2 location=embedded
Signature=adhoc
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements count=1 size=60

Two states, two different conversations with every gate on this map. The unsigned script reports code object is not signed at all on inspection. The path suffix varies per run. After codesign --force --sign -, that file carries a code directory and the flags=0x2(adhoc) marker. Ad hoc signing provides integrity but no identity. It satisfies "must have some signature" checks while telling every verifier that nobody stands behind the code.

These are the states you will keep straight for the rest of the course:

StateIdentityIntegrityWhat it gets you
UnsignednonenoneNo signature identity; launch result depends on policy and context
Ad hocno identified developeryesHas a code directory but no Developer ID identity
Developer IDan Apple-issued teamyesRecognized as an identified developer
Developer ID plus notarizationteam plus Apple ticketyesCan satisfy the applicable first-launch policy
PlatformApple platform signatureyesSystem trust subject to the relevant policy

AMFI and Hardened Runtime: Enforcement at Load Time

Code signing is the identity card. AMFI, the Apple Mobile File Integrity agent, is the guard who checks it. AMFI lives in the kernel and consults a userland daemon, amfid, to validate signatures as code loads. Confirm both are alive on your box:

the enforcement daemons
% ps ax -o pid,user,comm | grep -E "amfid|syspolicyd" | grep -v grep
  108 root             /usr/libexec/amfid
222 root             /usr/libexec/syspolicyd

Both run as root, which is exactly what you want from the guards. amfid validates code signatures at load time. syspolicyd evaluates system policy decisions, including Gatekeeper assessments, which you will meet in the next section.

The Hardened Runtime is a set of restrictions a developer opts into at build time, and Apple requires it for notarization. It blocks DYLD environment variable injection, unsigned library loading, debugger attachment, and JIT allocation unless specific entitlements lift each restriction. On this build, the Foundation Models CLI is a useful system-binary example:

hardened runtime on the fm CLI
% codesign -dv --verbose=1 /usr/bin/fm 2>&1 | head -12
Executable=/usr/bin/fm
Identifier=com.apple.fm
Format=Mach-O universal (x86_64 arm64e arm64e.x1)
CodeDirectory v=20500 size=2477 flags=0x10000(runtime) hashes=67+7 location=embedded
Platform identifier=26
Signature size=4202
Signed Time=Aug 20, 2026 at 10:40:26 PM
Info.plist=not bound
TeamIdentifier=not set
Runtime Version=27.0.0
Sealed Resources=none
Internal requirements count=1 size=60

The line that matters is flags=0x10000(runtime). That flag is the Hardened Runtime. Ordinary DYLD environment injection into this hardened target is restricted; check the exact entitlements and policy before claiming an exception. Phase 03 will show you what hardened targets do and do not allow, using this exact flag as the decision point.

Defenders look at the kernel's boot arguments for weakened policy, so you should know what yours hold:

boot arguments
% sysctl kern.bootargs
kern.bootargs:

The value after kern.bootargs: is empty, which is the clean state: no boot arguments are set. If a target ever shows boot arguments that weaken AMFI or disable library validation, that is both an operator opportunity and a detection artifact of the first order.


Gatekeeper, Quarantine, and Notarization: The First Launch Gate

Gatekeeper assesses software at launch under the host's policy. It is not a runtime guard. Quarantine commonly triggers the first-open assessment, while the code signature and notarization status inform the verdict. Absence of a quarantine attribute in this file probe does not prove that every launch path will skip assessment.

Two probes establish the gate. The first reads the policy, the second asks Gatekeeper to assess a known-good app so you can see its verdict format:

Gatekeeper policy and verdict
% spctl --status
% spctl --assess --verbose=4 /System/Applications/TextEdit.app 2>&1
assessments enabled
/System/Applications/TextEdit.app: accepted
source=Apple System

assessments enabled is the default and the state you should assume on any target. accepted with source=Apple System is the verdict shape you will learn to read. When your own delivered artifact is assessed, the source line is what tells you which trust path it took.

Quarantine is an extended attribute commonly applied by a download agent. Its fields can identify the agent and event, but the value alone does not prove a particular launch verdict.

quarantine marker, hands on
% QFILE=$(mktemp "${TMPDIR:-/tmp}/msl-0.2-quarantine.XXXXXX")
% printf 'lab test\n' > "$QFILE"
% xattr -w com.apple.quarantine "0083;68ab6c98;Safari;E7C1A1D6-9C4E-4B5D-9C6C-1F2D3E4A5B6C" "$QFILE"
% xattr -l "$QFILE"
% rm -- "$QFILE"
com.apple.quarantine: 0083;68ab6c98;Safari;E7C1A1D6-9C4E-4B5D-9C6C-1F2D3E4A5B6C

The format is four fields: a flag word, a timestamp in hex, the agent that downloaded the file, and a UUID for the event. Two operator facts about quarantine will matter to you constantly:

  1. Propagation depends on the source attribute, copy or extraction tool, and destination. Module 0.4 measures specific paths. Do not assume that cp, tar, ditto, or unzip always preserve or always remove the attribute. Inspect the delivered artifact before making a first-launch prediction.
  2. Removing quarantine with xattr -d changes file metadata. That change can be observed by a collector with suitable file telemetry; it does not establish that every host logs the deletion or that no later assessment occurs.

Notarization is the last piece. A developer uploads software to Apple, Apple runs automated scanning, and a ticket is issued. A developer can staple the ticket to the app, or Gatekeeper can obtain it online when connectivity and policy allow. Stapling supports offline verification; it is not mandatory for notarization. This is automated triage, not a security audit. For an operator it is a delivery constraint: notarized Developer ID apps pass Gatekeeper with the least friction, and the Developer ID certificate ties the binary to an identity, which becomes part of your attribution story. Note that platform apps like TextEdit do not carry third-party stapled tickets, so do not use them to learn what a stapled ticket looks like. Phase 01 shows you the real thing on a delivered artifact.


Sandbox: Containment by Profile

The sandbox confines a process to an allowlist: the files, network endpoints, Mach services, and resources its profile permits. App Store apps are sandboxed by default. Command line tools and daemons generally are not. The difference is visible in the entitlements, which are the declared privileges embedded in the signature.

Compare Apple's Calculator with your shell. First, extract the one entitlement that declares sandboxing:

Calculator declares the sandbox
% codesign -d --entitlements :- /System/Applications/Calculator.app 2>/dev/null \
% | grep -o '<key>com.apple.security.app-sandbox</key><true/>'
<key>com.apple.security.app-sandbox</key><true/>

The com.apple.security.app-sandbox entitlement set to true is the declaration. Calculator runs inside a container and reaches only what its profile allows. Now check the same entitlement on zsh:

zsh is not sandboxed
% if ENTITLEMENTS=$(codesign -d --entitlements :- /bin/zsh 2>/dev/null); then
% printf '%s\n' "$ENTITLEMENTS" | awk 'index($0, "<key>com.apple.security.app-sandbox</key><true/>") { found=1; print } END { if (!found) print "App Sandbox entitlement: absent" }'
% else
% echo 'codesign failed; inspect the signature before drawing a conclusion' >&2
% false
% fi
App Sandbox entitlement: absent

The displayed shell signature lacks the com.apple.security.app-sandbox entitlement; Calculator's signature includes it. That difference is an access-planning lead, not proof that one process can read every home path while the other can read none. The sandbox profile, grants, path, and actual caller still matter. Test the specific resource before claiming reachability. A defender may also collect sandbox denials, but this entitlement comparison did not capture one.


TCC: Consent as a Boundary

TCC, Transparency Consent and Control, is the privacy gate. When a process wants protected data, contacts, calendar, camera, microphone, location, full disk access, accessibility, screen recording, TCC checks applicable grants and policy. A prompt is one possible result, not the only one. Decisions may be stored in SQLite databases, and their location is worth inspecting:

the TCC stores on Golden Gate
% ls -la "/Library/Application Support/com.apple.TCC/"
% test -d "$HOME/Library/Application Support/com.apple.TCC" && echo "user TCC directory: present" || echo "user TCC directory: absent for this account"
total 184
drwxr-xr-x   4 root  wheel    128 Oct  1 16:50 .
drwxr-xr-x  11 root  admin    352 Sep 24 02:38 ..
-rw-r--r--   1 root  wheel  20480 Oct  1 16:05 REG.db
-rw-r--r--   1 root  wheel  73728 Oct  1 16:50 TCC.db
user TCC directory: absent for this account

Four lessons in that capture. First, the system database exists at /Library/Application Support/com.apple.TCC/TCC.db, owned by root. Second, this Golden Gate build keeps a companion store, REG.db, beside it. Record its presence and timestamps. The trust plane keeps more than one ledger, and your job is to notice what is there rather than assume. Third, the user-level directory does not exist for this account in this capture. That is a scoped filesystem observation, not a complete history of privacy decisions. Fourth, you can list these files but you cannot read the databases themselves without Full Disk Access, which is the point: the privacy store is itself private.

Two operator questions about TCC shape later phases. Which service is being requested, and which client and responsible process does policy attribute? Code signing can affect that identity, but this signing probe does not predict the next TCC decision. A prompt is one possible path, not a guarantee on every access. Phase 06 measures a specific service and caller before stating the boundary.


launchd: Domains Are Privilege

launchd is PID 1 and manages system and user service domains. It is not the direct parent of every process. It is also a privilege boundary, because the domain a service lives in decides who it runs as and when it starts. There are three locations you must keep straight:

LocationDomainRuns asStarts
/System/Library/LaunchDaemonssystemusually root; inspect UserNameboot or configured trigger
/Library/LaunchDaemonssystemusually root; inspect UserNameboot or configured trigger
~/Library/LaunchAgentsuser domainaccount owning the loaded joblogin or configured trigger

Look at your user domain, live:

the GUI domain for uid 501
% launchctl print "gui/$(id -u)" 2>&1 | head -13
gui/501 = {
type = login
handle = 100002
active count = 451
service count = 450
active service count = 197
creator = loginwindow[165]
creator euid = 0
session = Aqua
endpoint destination = com.apple.xpc.launchd.domain.user.501
auxiliary bootstrapper = com.apple.xpc.otherbsd (complete)
security context = {
	uid = 501

This capture is a logged-in Aqua session for uid 501. On your Mac, the UID will come from id -u; the GUI domain may be absent in an SSH-only or headless session. A LaunchAgent's actual domain and session type must be verified from its loaded job, not inferred from the plist location alone. Phase 07 builds on that distinction.

Now the Golden Gate detail that matters for both offense and detection. Count the daemon plists in the sealed system location versus the third-party location:

where the daemons live
% ls /System/Library/LaunchDaemons | wc -l
% ls /Library/LaunchDaemons | wc -l
     425
     0

This dedicated course VM had 425 plists in the sealed system location and none in /Library/LaunchDaemons when captured. The count is inventory, not a universal baseline. A future non-Apple entry in a writable launchd location is a triage lead; decide what it means from its owner, program path, signing identity, loaded domain, and change history.


MDM and Configuration Profiles: The Enterprise Overlay

Mobile Device Management is policy imposed by an organization above the local admin: Gatekeeper settings, certificates, enforced login items, FileVault and firewall policy, remote queries. On a managed Mac, your assumptions change, which is why enrollment status is one of the first discovery commands you run on any target. Run it on yours:

MDM enrollment status
% profiles status -type enrollment 2>&1
Enrolled via DEP: No
MDM enrollment: No

Your lab box is unenrolled, which is the clean research state. On a managed fleet the same command reports the enrollment and the MDM server, and every artifact you leave becomes potentially visible to the management channel. Phase 04 turns MDM discovery into a full enterprise footprint assessment. For this module, the rule is simple: query it early, and let the answer shape the rest of the operation.


Unified Logging: A Partial Boundary Record

Several subsystems in this trust map publish unified-log messages, including some policy evaluations, TCC decisions, sandbox denials, and launchd events. The store is not a complete record of every boundary interaction. Level, privacy, retention, and predicate choice limit what a local query returns; other collectors may see different evidence.

Query it for trust-related activity from the last few minutes:

an idle machine logs almost nothing
% /usr/bin/log show --last 5m --style compact \
% --predicate 'subsystem == "com.apple.syspolicy" OR process == "amfid"' 2>&1 | head -5

Output depends on activity in the five-minute window. On the validation VM, this query returned a header and an unrelated syspolicyd certificate-array error, not evidence of the assessment above. A result from this predicate, time window, log level, privacy setting, and retention state neither proves nor disproves that a particular policy decision occurred. Phase 04 teaches how to correlate an action with a scoped observation.


AI-Related Trust Boundaries on This Build

Apple's assistant and model services existed before macOS 27. On this build, the assistant is an application and a fleet of daemons that mediate actions, hold personal context, and carry private entitlements that no third-party code can obtain. For an operator, that makes the AI surface two things at once: a reconnaissance target, because its privileges define what the platform now trusts, and a discipline, because you must understand it before you can reason about any technique that touches it.

Start with the entitlements of the assistant itself. These are the declared privileges embedded in the signature of /System/Applications/Siri AI.app:

siri ai entitlement excerpt
% codesign -d --entitlements - "/System/Applications/Siri AI.app" 2>/dev/null \
% | grep -E "campo|Contacts.database|FaceTime.NoPrompt|Pasteboard.background|QuartzCore.global-capture|appleaccount|apfs.unlock|uibridge-service" \
% | sort -u | tee "$EVIDENCE/siri-ai-entitlements.txt"
			[String] com.apple.assistant.request-dispatcher.uibridge-service
		[String] com.apple.assistant.uibridge-service
	[String] com.apple.campo
[Key] com.apple.Contacts.database-allow
[Key] com.apple.FaceTime.NoPrompt
[Key] com.apple.Pasteboard.background-access
[Key] com.apple.QuartzCore.global-capture
[Key] com.apple.accounts.appleaccount.fullaccess
[Key] com.apple.apfs.unlock
[Key] com.apple.assistant.request-dispatcher.uibridge-service
[Key] com.apple.assistant.uibridge-service
[Key] com.apple.menubar.campo
[Key] com.apple.runningboard.assertions.angeltarget.campo

Thirteen lines, reproduced from this build's signature. Read them as declared capabilities, not proof that each operation succeeded in this session. The application identifier is com.apple.campo; several other keys refer to Contacts, FaceTime, pasteboard, capture, account, APFS, and UI bridge surfaces. None of those strings is a vulnerability by itself. The operator skill is to separate entitlement possession from actual operation and caller reachability. Save the list as a build-specific inventory, then test any proposed route under the relevant session and policy.

The assistant is not one process. It is a fleet. List the ones running in your session:

the intelligence daemon fleet
% launchctl list | grep -iE "intelligence|campo|siri" | head -14
699	0	com.apple.intelligenceplatformd
486	0	com.apple.campo
845	0	com.apple.intelligencetasksd
676	0	com.apple.siri.context.service
578	0	com.apple.CampoRemoteService
-	0	com.apple.SiriTTSTrainingAgent
-	0	com.apple.callintelligenced
653	0	com.apple.intelligenceflowd
697	0	com.apple.siriactionsd
598	0	com.apple.siriknowledged
1356	0	com.apple.siriappintentsd
690	0	com.apple.siriinferenced
666	0	com.apple.intelligencecontextd
510	0	com.apple.visualintelligenced

This logged-in validation session listed fourteen matching services; a dash means launchd has a registered job with no current PID. Membership and PIDs vary with the session and activity. Treat this as a session-local observation, not a complete roster. A broader inventory requires inspecting launchd plists and process state in the relevant user session. Compare each service's privileges and reachability before treating it as an operator path; a name alone proves neither when it first appeared nor who can reach it.

Model availability depends on the caller's context. Ask the Foundation Models CLI what it can establish in this session:

model availability is context gated
% fm available 2>&1 | tee "$EVIDENCE/fm-availability.txt"
System model unavailable: deviceNotEligible

The disposable macOS 27 VM returned deviceNotEligible after the CLI's legal notice was accepted. A physical host may differ. fm available is a caller/device eligibility check, not proof of a completed inference or of where a later request will be processed. It also does not establish Private Cloud Compute policy.

This build ships an Endpoint Security tracer at /usr/bin/estrace, a symlink to eslogger. It is a first-party tool you can use to measure specific events when the required privileges are available. Confirm that the tool exists before planning such a measurement:

the platform ships its own tracer
% ls -la /usr/bin/estrace /usr/bin/eslogger
-rwxr-xr-x  1 root  wheel  1372112 Sep 24 02:10 /usr/bin/eslogger
lrwxr-xr-x  1 root  wheel        8 Sep 24 02:10 /usr/bin/estrace -> eslogger

A dedicated lab later in the course, module 2.9, turns estrace into your visibility tester. For this module, it belongs on the map as an available measurement tool, not as proof of what any endpoint product collects.


One Launch, Every Boundary

The layers do not act alone, and the fastest way to internalize the map is to consider a single ordinary action through the stack. Suppose a user downloads a signed, notarized app, extracts it, and opens it. The checks below are questions to verify on that host, not a guaranteed event sequence:

  1. Did the download agent attach quarantine, and did extraction preserve it?
  2. Which app identity did LaunchServices receive at first open?
  3. Did policy assess the artifact, and which signature/notarization state was used? Was a ticket stapled or fetched online?
  4. What did code-signing and Hardened Runtime policy enforce at launch?
  5. If the app requested camera access, did TCC use an existing grant, managed policy, a prompt, or a denial?
  6. If sandboxed, which file or IPC action did the app attempt and was it allowed?
  7. Query the relevant logs and sensors for evidence of each step; not every transition is guaranteed to produce a retained, visible unified-log entry.

Content that an assistant app reads, summarizes, or acts on crosses a separate application trust boundary. Its configured tools, entitlements, user consent, and policy determine which actions are possible; model output alone grants no authority. Phase 01 tests that distinction in a lab app. For now, hold the lesson: any single operator action can touch five or more boundaries at once, and each contact is a place the action can fail and a place it leaves evidence.


Automate the Survey

You ran every probe by hand so you would learn what each one means. Now take the fast path. The phase pack ships a read-only survey script that runs the whole map and stores each result in your evidence directory:

run the trust survey
% ./scripts/trust-survey.sh
captured sip-status.txt (exit=0)
captured gatekeeper-status.txt (exit=0)
captured gatekeeper-assess.txt (exit=0)
captured codesign-zsh.txt (exit=0)
captured codesign-fm.txt (exit=0)
captured mdm-enrollment.txt (exit=0)
captured tcc-system.txt (exit=0)
captured tcc-user.txt (exit=1)
captured launchd-user.txt (exit=0)
captured nvram-boot-args.txt (exit=0)
captured amfid-process.txt (exit=0)
captured sealed-volume.txt (exit=0)
captured launchd-plist-counts.txt

survey complete: /Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey (nonzero probes: 1; inspect each exit_status)
amfid-process.txt
codesign-fm.txt
codesign-zsh.txt
gatekeeper-assess.txt
gatekeeper-status.txt
launchd-plist-counts.txt
launchd-user.txt
mdm-enrollment.txt
nvram-boot-args.txt
sealed-volume.txt
sip-status.txt
tcc-system.txt
tcc-user.txt

The survey stores each result under $EVIDENCE/trust-survey/ and does not change system policy. tcc-user.txt returned nonzero because that directory was absent in this VM session; the script records that status instead of presenting the probe as successful. Earlier signing and quarantine examples created and removed temporary files, and the evidence workspace was written. Close by confirming the workspace holds what you collected:

verify the workspace
% ./verify/verify-workspace.sh 0.2-trust-boundaries
== MacSec Labs workspace check: 0.2-trust-boundaries ==
PASS  evidence directory exists: /Users/tester01/macseclabs/evidence/0.2-trust-boundaries
INFO  non-empty evidence files: 17

evidence contents:
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/fm-availability.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/sealed-volume.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/sip-status.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/siri-ai-entitlements.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/amfid-process.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/codesign-fm.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/codesign-zsh.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/gatekeeper-assess.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/gatekeeper-status.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/launchd-plist-counts.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/launchd-user.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/mdm-enrollment.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/nvram-boot-args.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/sealed-volume.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/sip-status.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/tcc-system.txt
/Users/tester01/macseclabs/evidence/0.2-trust-boundaries/trust-survey/tcc-user.txt

result: workspace check passed

Seventeen files: the four captures you teed along the way, and the thirteen the survey wrote under trust-survey/. The verifier is read-only and you can run it any time. Run it early to confirm the workspace exists, and again at the end of a module to see the evidence you accumulated. That habit, proving what you collected, is what carries into every engagement report.


Detection engineering

This module issued a burst of read-only probes, including csrutil, spctl, codesign, launchctl, and log show, from the lab user's session. A sensor with suitable process and argument collection could record and correlate some of them. This lab did not run such a sensor or demonstrate a product alert, so it cannot claim that every command line was captured or flagged.

That is the point of doing it openly first. Reconnaissance of the trust surface is a MITRE ATT&CK discovery pattern, and you now know what yours looks like from the other side. The filesystem kept its own record too: your evidence directory now carries a trust-survey folder whose creation time, owner, and contents all testify to exactly this session.

In later phases you will learn to reduce this signature when the engagement requires it, and you will measure the reduction honestly. The reference point is the loud, complete, verifiable survey you just ran.


Checkpoint

Before module 0.3, confirm you can answer each of these without looking back:

  1. What does csrutil status say on your box, and why can you not change it from a normal session?
  2. What does the sealed flag on the root mount mean for where your payloads can live?
  3. Name the five code signing states and say what each one gives you.
  4. Which flag in a codesign -dv output marks the Hardened Runtime, and what did Apple's own fm CLI show for it?
  5. Why does an unsigned dylib fail against a hardened target? Which two boundaries team up to stop it?
  6. What did the TCC directory check establish for this account, and what host history cannot be inferred from that absence?
  7. How many third-party daemon plists sit in /Library/LaunchDaemons on your box, and why does that number matter for persistence OPSEC?
  8. Which entitlement on Siri AI surprised you most, and why?

If any answer is shaky, re-run that section's commands. The map is only useful if you can draw it from memory.

Module 0.3 takes this map and applies it to the living system: process identity, user context, and session authority, the layer that decides what your code runs as and what it can touch.

This is one free module of many.

Get every module across all ten phases, with working code and the detection engineering to catch each technique.

Get full access