Introduction to macOS Gatekeeper Security
macOS Gatekeeper is a core security feature introduced by Apple in OS X Mountain Lion (10.8) that protects Mac users from running malicious or untrusted software. As a developer, understanding how Gatekeeper works is essential if you plan to distribute applications outside the Mac App Store. Gatekeeper acts as the first line of defense by verifying the identity of developers and ensuring that applications have not been tampered with since they were signed.
At its core, Gatekeeper evaluates apps downloaded from the internet and determines whether they are safe to launch. It uses a combination of code signing, notarization, and quarantine attributes to make this decision. For developers building tools, utilities, or full applications for macOS, mastering Gatekeeper is not optional — it is a requirement for delivering a smooth user experience and maintaining trust.
What Is Gatekeeper and How Does It Work?
Gatekeeper is a security mechanism built into macOS that enforces code signing and notarization policies on applications. When a user downloads an application through a browser, email client, or other internet-aware application, macOS attaches a special extended attribute called the com.apple.quarantine flag to the downloaded file. This flag marks the file as untrusted until Gatekeeper has verified it.
When the user attempts to open a quarantined application, the system performs a series of checks:
- Quarantine check: The system verifies whether the file has the quarantine attribute set.
- Code signature validation: The system checks whether the app is signed with a valid Developer ID certificate issued by Apple.
- Notarization check: On macOS 10.15 and later, the system verifies that the app has been notarized by Apple's automated security scanning service.
- Stapled ticket check: If the app is distributed offline, the system looks for a stapled notarization ticket attached to the app bundle.
If any of these checks fail, macOS will display a warning dialog preventing the user from launching the application. The user may see messages such as "cannot be opened because the developer cannot be verified" or "is damaged and can't be opened." These messages are Gatekeeper's way of protecting users from potentially harmful software.
The Quarantine Attribute
The quarantine attribute is the trigger that activates Gatekeeper checks. You can inspect whether a file has this attribute using the xattr command-line tool:
# Check extended attributes of a downloaded file
xattr ~/Downloads/MyApp.app
# Output might look like:
# com.apple.quarantine
# Display the full quarantine attribute value
xattr -p com.apple.quarantine ~/Downloads/MyApp.app
# Remove the quarantine attribute (for testing purposes only)
xattr -d com.apple.quarantine ~/Downloads/MyApp.app
Removing the quarantine attribute bypasses Gatekeeper entirely, which is why it should only be used for legitimate testing. In production, users should never need to do this if your app is properly signed and notarized.
Why Gatekeeper Matters for Developers
Gatekeeper matters because it directly affects whether users can run your software. If you distribute an unsigned or unnotarized application, users will encounter intimidating warning dialogs that may prevent them from ever launching your product. This can lead to support tickets, negative reviews, and lost customers.
From a security perspective, Gatekeeper establishes a chain of trust. Apple issues Developer ID certificates only to developers who have enrolled in the Apple Developer Program and passed identity verification. When you sign your app with this certificate, users can be confident that the software came from you and has not been modified by a third party. Notarization adds an additional layer by running your app through Apple's automated malware scanning before distribution.
For enterprise developers and system administrators, Gatekeeper also provides policy controls that can be configured via MDM (Mobile Device Management) solutions. This allows organizations to enforce strict rules about which applications can run on corporate Macs, improving overall security posture.
Code Signing Your Application
Code signing is the foundation of Gatekeeper compliance. To sign your application, you need an Apple Developer ID Application certificate, which you can obtain through the Apple Developer Program. Once you have the certificate installed in your keychain, you can sign your app using the codesign tool.
Signing an Application Bundle
Here is a basic example of signing a macOS application bundle:
# Sign the application with your Developer ID
codesign --force --deep --options runtime \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
/path/to/MyApp.app
# Verify the signature
codesign --verify --verbose=4 /path/to/MyApp.app
# Display detailed signing information
codesign -dvvv /path/to/MyApp.app
The --options runtime flag enables the Hardened Runtime, which is required for notarization. The Hardened Runtime protects your app from code injection, DLL hijacking, and process memory space tampering. It also restricts certain capabilities that must be explicitly enabled through entitlements.
Adding Entitlements
If your app requires capabilities that the Hardened Runtime restricts by default, you need to declare entitlements in a property list file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Apply the entitlements during the signing process:
codesign --force --options runtime \
--entitlements MyApp.entitlements \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
/path/to/MyApp.app
Notarizing Your Application
Notarization is Apple's process of scanning your software for malicious content and verifying that it is properly signed. Since macOS 10.15 Catalina, notarization is mandatory for all software distributed outside the Mac App Store. The notarization process involves submitting your app to Apple's servers, waiting for automated analysis, and then stapling the resulting ticket to your application.
Submitting for Notarization
Before submitting, you must archive your application into a zip file or create a disk image (DMG). Then use the notarytool command to submit it to Apple:
# Create a zip archive of the application
ditto -c -k --keepParent /path/to/MyApp.app MyApp.zip
# Submit the archive for notarization
xcrun notarytool submit MyApp.zip \
--apple-id "your@email.com" \
--password "app-specific-password" \
--team-id "TEAM_ID" \
--wait
# Check the status of a submission
xcrun notarytool info <submission-id> \
--apple-id "your@email.com" \
--password "app-specific-password" \
--team-id "TEAM_ID"
The --wait flag tells the tool to block until the notarization process completes. This can take anywhere from a few minutes to over an hour depending on the size of your application and Apple's server load.
Stapling the Notarization Ticket
Once notarization succeeds, you should staple the notarization ticket to your application. This ensures that Gatekeeper can verify the app even when the user's Mac is offline:
# Staple the ticket to the application
xcrun stapler staple /path/to/MyApp.app
# Verify that the staple is valid
xcrun stapler validate /path/to/MyApp.app
# Perform a final Gatekeeper assessment
spctl --assess --verbose=4 /path/to/MyApp.app
The spctl command (Security Assessment Policy) performs the same check that Gatekeeper runs when a user opens the app. If this command succeeds, your app is ready for distribution.
Building a Complete Notarization Script
To streamline the notarization workflow, most developers create an automated script that handles signing, archiving, submitting, stapling, and verification. Here is a complete example:
#!/bin/bash
set -euo pipefail
APP_PATH="build/MyApp.app"
APP_NAME="MyApp"
ZIP_PATH="build/MyApp.zip"
SIGN_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
APPLE_ID="your@email.com"
APP_PASSWORD="app-specific-password"
TEAM_ID="TEAMID123"
echo "Step 1: Cleaning previous builds..."
rm -rf "$ZIP_PATH"
echo "Step 2: Verifying code signature..."
codesign --verify --verbose=4 "$APP_PATH"
echo "Step 3: Creating zip archive..."
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "Step 4: Submitting for notarization..."
xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_PASSWORD" \
--team-id "$TEAM_ID" \
--wait
echo "Step 5: Stapling notarization ticket..."
xcrun stapler staple "$APP_PATH"
echo "Step 6: Validating staple..."
xcrun stapler validate "$APP_PATH"
echo "Step 7: Running Gatekeeper assessment..."
spctl --assess --verbose=4 "$APP_PATH"
echo "Notarization complete! $APP_NAME is ready for distribution."
Save this script as notarize.sh, make it executable with chmod +x notarize.sh, and run it after building your application. This script provides a repeatable, auditable process that reduces the chance of human error during distribution preparation.
Distributing Disk Images (DMG)
Many macOS applications are distributed as DMG files. When you distribute a DMG, both the application inside and the DMG itself must be signed and notarized. Here is how to create and notarize a DMG:
# Create a read-write DMG
hdiutil create -volname "MyApp" \
-srcfolder /path/to/distribution_folder \
-ov -format UDRW MyApp_temp.dmg
# Convert to a compressed read-only DMG
hdiutil convert MyApp_temp.dmg \
-format UDZO \
-o MyApp.dmg
# Sign the DMG
codesign --sign "Developer ID Application: Your Name (TEAM_ID)" \
--options runtime MyApp.dmg
# Submit the DMG for notarization
xcrun notarytool submit MyApp.dmg \
--apple-id "your@email.com" \
--password "app-specific-password" \
--team-id "TEAM_ID" \
--wait
# Staple the notarization ticket to the DMG
xcrun stapler staple MyApp.dmg
# Validate the DMG
xcrun stapler validate MyApp.dmg
It is important to notarize the DMG separately from the app inside it. Gatekeeper checks the outermost container first, so if the DMG is not notarized, the user will see a warning even if the app inside is properly signed and notarized.
Best Practices for Gatekeeper Compliance
- Always enable the Hardened Runtime: This is mandatory for notarization and provides critical protections against code injection attacks.
- Sign all executable code: This includes helper tools, embedded frameworks, dynamic libraries, and any command-line tools bundled with your app. Use the
--deepflag carefully, as Apple recommends signing nested components individually for better control. - Minimize entitlements: Only request the entitlements your app absolutely needs. Each additional entitlement expands the attack surface of your application.
- Automate the notarization process: Use scripts or CI/CD pipelines to ensure consistent signing and notarization across every release. Manual processes are error-prone.
- Test on a clean system: After notarizing, test your app on a fresh macOS installation or a new user account to confirm Gatekeeper accepts it without warnings.
- Keep your certificates current: Developer ID certificates expire. Monitor expiration dates and renew them well in advance to avoid distribution interruptions.
- Store credentials securely: Use App Store Connect API keys or store app-specific passwords in your CI system's secret management rather than hardcoding them in scripts.
- Version your builds: Always increment build numbers between notarization submissions to avoid conflicts with Apple's servers.
Troubleshooting Common Gatekeeper Issues
Even experienced developers encounter Gatekeeper problems. Here are some common issues and how to resolve them:
"App is damaged and can't be opened"
This misleading message often appears when an app is not properly notarized or when the notarization ticket is missing. Check the notarization status and re-staple the ticket:
# Check if the app has a valid notarization ticket
xcrun stapler validate /path/to/MyApp.app
# Re-staple if needed
xcrun stapler staple /path/to/MyApp.app
# Check the notarization log for details
xcrun notarytool log <submission-id> \
--apple-id "your@email.com" \
--password "app-specific-password" \
--team-id "TEAM_ID"
Signature Validation Failures
If codesign --verify fails, inspect the app for unsigned resources or nested code:
# Perform a strict verification
codesign --verify --strict --verbose=2 /path/to/MyApp.app
# List all signed code in the bundle
codesign -d --recursive --verbose=4 /path/to/MyApp.app
# Check for unsigned files
find /path/to/MyApp.app -type f -exec sh -c \
'codesign -v "$1" 2>/dev/null || echo "UNSIGNED: $1"' _ {} \;
Entitlement-Related Crashes
If your app crashes after enabling the Hardened Runtime, it may need additional entitlements. Check the crash log for messages about missing entitlements or restricted operations. Common entitlements needed include com.apple.security.cs.allow-jit for JIT compilers and com.apple.security.cs.disable-library-validation for apps that load third-party plugins.
Conclusion
macOS Gatekeeper is a critical security mechanism that every macOS developer must understand and accommodate. By properly code signing your applications with a Developer ID certificate, enabling the Hardened Runtime, notarizing through Apple's automated service, and stapling the resulting ticket, you ensure that users can install and run your software without encountering frightening warning dialogs. While the process involves multiple steps and command-line tools, automating it through scripts and CI pipelines makes it manageable and reliable. Investing the effort to fully comply with Gatekeeper requirements not only protects your users but also builds trust in your brand and reduces the support burden associated with distribution issues. As Apple continues to tighten security requirements with each macOS release, staying current with Gatekeeper best practices will remain an essential skill for any serious macOS developer.