macOS Security Overview

Apple provides multiple security mechanisms in macOS that rely on each other.



App Execution Security

  • App Sandbox: isolates an app’s activities to limit potential damage.
  • Gatekeeper: ensures only trusted apps can run.
  • Notarization & Developer ID: verifies software integrity and developer legitimacy.

Gatekeeper

Gatekeeper controls which applications can be executed in macOS by:
  • Checking for a valid Developer ID signature from Apple, so that malicious developers can be revoked if necessary.
  • Requiring apps to pass Apple’s notarization process, which scans software for suspicious content and security vulnerabilities.

Notarization & Developer ID

Notarization & Developer ID work together to validate the origin and safety of an app:
  • Developer ID: A unique identifier Apple provides to trusted developers, helping users recognize legitimate software sources.
  • Notarization: A process where Apple’s automated systems scan and approve apps, identifying malicious code or security flaws before they reach your Mac.

App Sandbox

The App Sandbox confines each application to a restricted environment:
  • Prevents an app from freely reading or modifying files outside its container.
  • Limits network access and system-level changes to minimize damage if the app is compromised.

System-Level Security

  • Secure Enclave: provides hardware-level cryptographic security.
  • SIP (System Integrity Protection): protects critical system files.
  • XProtect: built-in anti-malware that scans for known threats.

SIP (System Integrity Protection)

SIP defends against unauthorized modifications to core macOS components:
  • Restricts the root user from altering protected files and folders.
  • Ensures only Apple-signed processes can make system-level changes.

XProtect

XProtect is Apple’s built-in anti-malware solution:
  • Uses Apple-updated malware definitions to detect known threats.
  • Operates behind the scenes, automatically blocking harmful software.

Secure Enclave

The Secure Enclave is a dedicated coprocessor that:
  • Manages sensitive data like biometric information (e.g., Touch ID).
  • Performs cryptographic operations in a secure, hardware-isolated environment.

Data & Privacy Protection

  • FileVault: provides full-disk encryption to protect your files.
  • Keychain: stores passwords and credentials securely.
  • TCC (Transparency, Consent, & Control): manages permissions for sensitive data.

TCC (Transparency, Consent, & Control)

TCC governs app requests to sensitive resources:
  • Prompts for consent when an app wants to access things like your camera, microphone, or contacts.
  • Allows you to review and adjust permissions in System Settings at any time.

FileVault

FileVault encrypts your disk to protect data at rest:
  • Uses XTS-AES-128 encryption to secure the entire startup volume.
  • Helps prevent unauthorized access if your Mac is lost or stolen.

Keychain

Keychain securely stores credentials and secrets:
  • Encrypts your passwords, certificates, and private keys.
  • Relies on the system’s security frameworks to prevent unauthorized access.

The Accessibility Permission

Accessibility permission falls under the TCC framework. It opens your system to programmatic control for certain purposes.

Accessibility Examples

Examples of applications that need accessibility:
  • Automation utilities
  • Window management tools
  • Voice control applications
  • Screen readers for visually impaired users
Examples of application control:
  • Simulated keyboard and mouse input
  • Monitor user interactions with other applications
  • Access and manipulate content from windows and controls across applications

Applications with accessibility permission are also referred to as trusted. macOS requires explicit user permission to designate an app as trusted. This takes the form of a dialog that requires admin authentication, and a Settings panel where permissions can be revoked. Spoiler alert: there is no clean way around manual authentication.

Configuration

To be able to request accessibility permissions you need to

  • Declare an entitlement if you run on a hardened runtime.
  • Declare the reason you need accessibility in your Info.plist.
  • Include a provisioning profile if you plan to distribute in the App Store.

In the entitlements file:

<key>com.apple.security.accessibility</key>
<true/>

In the Info.plist:

<key>NSAccessibilityUsageDescription</key>
<string>Accessibility permissions are used for window management.</string>

Request Accessibility

To find out if the application has permissions:

// Returns TRUE if the current process is a trusted accessibility client
let isGranted = AXIsProcessTrusted()

To prompt the user for permissions:

let options: NSDictionary = [
    kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true
]
AXIsProcessTrustedWithOptions(options)

The code above will either present a system dialog, or redirect the user to the Accessibility panel at Settings > Privacy & Security > Accessibility. Such panel can also be open programmatically with:

if let url = URL(string: 
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
) {
    NSWorkspace.shared.open(url)
}

Grant accessibility

A workflow that is reliable is to

  1. Build the application
  2. Remove and add back the application to Accessibility.
  3. Run the application from Xcode > Product > Perform Action > Run Without Building

You will be able to debug the application, but it is of course, very inconvenient to go through all this after each build.

TCC provides a utility (tccutil) that can reset permissions, but not assign them. The TCC database is a SQLite file on disk you can read, but writing requires disabling SIP, which opens the possibility to make a catastrophic mistake that damages your system.

Disabling SIP

  1. Turn on the Mac and press ⌘R until you see the Apple logo or the recovery utilities.
  2. Go to the Utilities menu at the top and select Terminal.
  3. Type: csrutil disable
  4. Restart the Mac.
Once restarted type crsutil status to verify it is disabled.
To re-enable SIP repeat the steps above but run csrutil enable.

TCC.db

# clears accessibility for one app
sudo tccutil reset Accessibility $PRODUCT_BUNDLE_IDENTIFIER

# clears accessibility for all apps
tccutil reset Accessibility

TCC.db

# is my app authorized? 1=granted, 2=denied
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT auth_value FROM access WHERE service='kTCCServiceAccessibility' AND client='dev.jano.orange';"

# read row
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT * FROM access WHERE service='kTCCServiceAccessibility' AND client='dev.jano.orange';"

# write (if SIP is disabled)
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "UPDATE access SET auth_value = 1 WHERE service = 'kTCCServiceAccessibility' AND client = 'dev.jano.orange';"

# list clients allowed in my machine
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT client FROM access WHERE service='kTCCServiceAccessibility';"

# show all services tracked there
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "SELECT DISTINCT service FROM access;"

The service, client, and auth_value are well known. The others are not documented, but more or less this is the unofficial table structure

Column Example Value Description
service kTCCServiceAccessibility The type of permission
client dev.jano.orange The bundle identifier
client_type 0 0 typically means bundled app
auth_value 2 2 usually indicates denied, 1 is allowed
auth_reason 4 Reason code for the authorization
auth_version 1 Version of the authorization
csreq ?? Code signing requirement
policy_id null Policy identifier
indirect_object_identifier 0 Used for file/folder access permissions
flags UNUSED Flag settings
last_modified null Last modification timestamp
pid 0 Process ID
expired_at 1736658020 Unix timestamp when permission expires
remote_pid null Remote process ID
responsibility_id UNUSED Responsibility identifier
temporary_grant 0 Whether this is a temporary permission

There is an utility in GitHub: tccprofile but didn’t look into it. There is another local database at ~/Library/Application\ Support/com.apple.TCC/TCC.db but it is not relevant.

Recommendations

(aka things to do to avoid problems)

Sign the application to make its identity clear to macOS. What I read online is that each build changes the app signature so permissions have to be requested again. But what I experienced is that signing with a Developer ID certificate is enough for the system to recognize it is the same app.

To sign your app

  1. Go to Signing & Capabilities
  2. Set a Team
  3. There are several options here. I use a Developer ID certificate and a Developer ID profile tied to it.
  4. Once signed, you can check the state of your app with codesign.

codesign

Verifying the application is signed.

% codesign -d -vv OrangeApp.app

Executable=Foo/Build/Debug/OrangeApp.app/Contents/MacOS/OrangeApp
Identifier=dev.jano.orangeapp
Format=app bundle with Mach-O thin (arm64)
CodeDirectory v=20400 size=650 hashes=10+6 location=embedded
Signature size=4691
Authority=Apple Development: Ellie Williams (H6L34F7G12) 👈
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Signed Time=Jan 12, 2025 at 09:10:19 👈
Info.plist entries=28
TeamIdentifier=PPSA6C3P8Q 👈
Sealed Resources version=2 rules=13 files=2
Internal requirements count=1 size=183

Copy the product to a stable location. What I read is that the default location of ~/Library/Developer/Xcode/DerivedData/ is hidden and changes between builds, which may confuse the system. But what I experienced is that the system recognizes the application regardless where it is generated.

FYI: changing the destination folder may cause problems if one of your dependencies declares a localization bundle of its own. If this is your case, you can still work around it and change the destination folder.

There are three ways to declare a custom location:

  • Set a build property.
  • Xcode > project name > Build Settings > Build Location > Per-configuration Build Products Path.
  • Product > Scheme > Edit Scheme… > Run > Build Configuration > Debug > Options > Build Location > Custom

Build property

This will place the product in Build/Debug/YourApp.app

CONFIGURATION_BUILD_DIR =
$(PROJECT_DIR)/Build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

Troubleshooting

The Console.app is your friend –despite its unfriendly UI. You can spot the Accesibility permission being denied if you paste the following in the search field (top, right):

type:error
subsystem:com.apple.sandbox.reporting
category:violation

The error will say something like

Sandbox: OrangeApp(16745) deny(1) mach-lookup com.apple.universalaccessAuthWarn

Another way to keep an eye on the console is to stream from the terminal:

log stream --predicate \
'process == "tccd" OR process == "OrangeApp" OR process="sandboxd"'