Get Password from Keychain in Shell Scripts

MacAdmin scripts often require passwords, mostly for interactions with APIs.

It is easiest to store the password in clear text, but that is obviously a terrible solution from a security perspective. You can pass the password as an argument to your script, but that is inconvenient and may still appear in clear text in the ps output or the shell history.

You can obfuscate the password with base64, but that is easily reversible. You can even try to encrypt the password, but since the script needs to be able to decrypt the password, you are just adding a layer of complexity to the problem.

macOS has a keychain, where the user can store passwords and allow applications and processes to retrieve them. We can have our script retrieve a password from a local keychain.

There are limitations to this approach:

  • the password item has to be created in the keychain
  • the user has to approve access to the password at least once
  • the keychain has to be unlocked when item is created and when the script runs—this usually requires the user to be logged in
  • the user and other scripts can find and read the password in the Keychain Access application or with the security tool

Because of these limitations, this approach is not useful for scripts that run without any user interaction, e.g. from a management system. Since the user can go and inspect the key in the Keychain Access is also not well suited for critical passwords and keys.

However, it is quite useful for workflow scripts that you run interactively on your Mac. This approach has the added benefit, that you do not have to remember to remove or anonymize any keys or passwords when you upload a script to GitHub or a similar service.

Note: Mischa used this in his ‘OnAirScanner’ script.

Update: I didn’t remember this, but Graham Pugh has written about this before.

How to Store a Password in the Keychain

Since adding the password to your keychain is a one-time task, you can create the password manually.

Open the Keychain Access application and choose “New Password Item…” from the Menu. Then enter the Keychain Item Name, Account Name and the password into the fields. The “Keychain Item Name” is what we are going to use later to retrieve the password, so watch that you are typing everything correctly.

You can also add the password from the command line with the security command.

> security add-generic-password -s 'CLI Test'  -a 'armin' -w 'password123' 

This will create an item in the Keychain with the name CLI Test and the account name armin and the horribly poor password password123.

How to Retrieve the Password in the Script

To retrieve a password from the keychain in a script, use the security command.

> security find-generic-password -w -s 'CLI Test' -a 'armin'

This will search for an item in the keychain with a name of CLI Test and an account name of armin. When it finds an item that matches the name and account it will print the password.

The first time you run this command, the system will prompt to allow access to this password. Enter your keychain password and click the ‘Always Allow’ button to approve the access.

This will grant the /usr/bin/security binary access to this password. You can see this in the Keychain Access application in the ‘Access Control’ tab for the item.

When you create the item with the security add-generic-password binary, you can add the -T /usr/bin/security option to immediately grant the security binary access.

Whether you grant access through the UI or with the command, keep in mind that a every other script that uses the security binary will also gain access to this password.

For very sensitive passwords, you can just click ‘Allow’ rather than ‘Always Allow.’ Then the script will prompt interactively for access every time. This is more secure, but also requires more user interaction.

Once you have tested that you can retrieve the password in the interactive shell, and you have granted access to the security binary, you can use command substitution in the script to get the password:

cli_password=$(security find-generic-password -w -s 'CLI Test' -a 'armin')

This command might fail for different reasons. The keychain could be locked, or the password cannot be found. (Because it was either changed, deleted or hasn’t been created yet.) You want to catch that error and exit the script when that happens:

pw_name="CLI Test"
pw_account="armin"

if ! cli_password=$(security find-generic-password -w -s "$pw_name" -a "$pw_account"); then
  echo "could not get password, error $?"
  exit 1
fi

echo "the password is $cli_password"

Twenty Years of Mac OS X

Mac OS X 10.0 was released March 24, 2001. Twenty years ago today.

The PowerBook, iMac, iPod, iPhone, iPad, and Apple Watch are obviously big steps along the way from Apple as a ‘beleagured,’ minor computer maker with an uncertain future to the $2 trillion tera-corp they are today. It is easy to focus on the hardware. But Mac OS X was at least as important.

Back then, it was essential that Apple move forward from ‘classic’ Mac OS. Protected memory, multi-user setups, and support for multiple applications running safely side-by-side were the main advantages of Mac OS X over Mac OS 9. But Mac OS X also brought with it the Unix core (and shell), a new display technology, and the Cocoa frameworks.

The transition was rough for the existing Mac users. The early versions were not as complete and stable as one would have hoped. The processing requirements of early Mac OS X pushed existing Mac hardware to their limits. Many application vendors dragged their feet adopting Mac OS X and the new technologies and features available.

But Mac OS X made the Mac interesting for a whole new group of people. It was the only platform at then time that they had Microsoft and Adobe productivity app as well as the Unix shell and tools available. This was a huge bonus for web designers and developers, but also for scientists.

Apple built some of the best laptops of the time. It might seem strange now, but having a portable, battery-powered Unix workstation, which also ran Word, Excel, Photoshop, and could edit videos, was un-imaginable just a few years before.

Then, Apple stripped down Mac OS X, so it could run on on a phone. Up until then, portable devices had very basic and minimal operating systems. They were also locked down and installing additional software was complicated and often expensive.

The early versions of iPhone OS were also basic and minimal compared to Mac OS X, but they held the promise of extension and growth. The iPhone had potential and Apple delivered on that promise with every system update. They treated the iPhone as a computer, rather than a gadget. It took a few years, but with the iPhone, people went from having one computer, to two computers: one in your pocket and one on your desk or in your bag.

Today, Apple has a range of operating systems from the watch on your wrist to the large screen in your living room, all going back to Mac OS X 10.0 twenty years ago. macOS is just one element in this ecology of devices. We don’t just have one computer, we have many. A spectrum of computers, most of them wireless and battery-powered, each with different strengths. These computers might all be from Apple, or from a variety of vendors.

macOS is part of this spectrum. In the past years, Apple has changed the name and just last year the major version number of their operating system for laptops and desktops.

Sometimes it seems that Apple has lost sight of what makes Macs an important tool. With Apple silicon for Macs, it seems that Apple is re-focusing on the Mac and seeing how they can improve macOS, while also improving the eco-system as a whole.

“macOS 11” holds a promise for continued and even re-newed growth. Like the first “Mac OS X” twenty years ago, and the first iPhone OS, there is potential.

I am looking forward to the next twenty years!

EraseInstall application retired

It makes me very sad that the EraseInstall application has been retired.

We built this tool three years ago, mostly because we wanted to learn how to build an app like this on macOS. We think it worked out well. We learned a lot, and are glad the application was useful to some.

Since then, all the people involved in the EraseInstall Project have moved on to other jobs or other responsibilities. Unfortunately, this leaves us with no time or resources to maintain or improve EraseInstall.

The repository and the application will remain available in its current state. There will be no more updates. If someone feels they can take up the project and continue it, please do!

If you are looking for a similar solution, we recommend Graham Pugh’s eraseinstall script.

Thank you, all, again!

Team EraseInstall: Mischa van der Bent, Arnold Nefkens, and Armin Briegel

Platform Support in macOS Installer Packages (pkg)

Mac users and admins find themselves in yet another major platform transistion. For the duration of the transition, developers and admins will have to deal with and support software and hardware for the Intel and Apple silicon Macs. With Universal applications and Rosetta 2, Apple is providing very efficient tools to dramatically reduce the friction and problems involved.

This post was inspired by comments from Josh Wisenbaker on MacAdmins Slack and Twitter. Thank you!

For most end user level tasks, these tools will provide seamless experience. Universal applications will run on either platform natively and Rosetta 2 will translate applications compiled for the legacy platform (Intel) so they can run on the new Apple silicon chips. There are only a few situations where these tools don’t work: virtualization solutions and Kernel extensions.

In most cases this tools will “just work.” But for MacAdmins there is one major issue that may throw a wrench in your well-oiled deployment workflows. Rosetta is not pre-installed on a fresh macOS installation.

We can only speculate why Apple chooses to deliver Rosetta this way. In “normal” unmanaged installations, this is not a big deal. The first time a user installs or launches a solution that requires Rosetta, they will be prompted to for installation and upon approval, the system will download and install Rosetta.

As a MacAdmin, however, you want your deployments to be uninterrupted by such dialogs. Not only are they confusing to end users, but the user might cancel out of them which will result in your workflow failing partially.

There are two solutions. The first is to install Rosetta as early as possible in the deployment process. Apple provides a new option for the softwareupdate command to initiate the installation. Graham Gilbert and Rich Trouton have already published scripts around this. Have this script run early in your deployment workflow on Apple silicon and subsequent apps and tools that require Rosetta should be fine.

The other solution is to avoid requiring Rosetta and thus the prompt for Rosetta.

I mentioned earlier that we can only speculate as to why Apple has made Rosetta 2 an optional installation. One possible explanation is, that Apple believes Rosetta will not be a necessary installation for very long. An extra dialog and installation will make users and developers more aware of software that “needs an update” and motivate developers to provide Universal applications faster.

When a user opens an application that requires Rosetta for the first time, before Rosetta is installed, the system prompts to install. The same thing can happen with an installer package. The system might prompt to install Rosetta before a certain package is installed. However, not all packages trigger the dialog. I was curious what is required in the package to trigger or to avoid the prompt.

Aside from legacy formats, there are two types of packages. The first are “plain” packages, which are also called component packages. These packages have a payload and can have pre- and postinstall scripts, but other than that, there is little metadata you can add to influence the installation workflow.

This is where “distribution packages” come in. Distribution packages do not have a payload or installation scripts of their own, but contain one or more component packages. In addition, distribution packages can contain metadata that influences the installation workflow, such as customization of the Installer.app interface, system version checks, prompting the user to quit running applications before an installation and software requirements and a few more.

Note: learn more about the detailed differences between component and distribution packages in my book: “Packaging for Apple Administrators

You can build a distribution package from a component package with the productbuild command:

> productbuild --package component.pkg distribution.pkg

Since most of the extra features of distribution packages are only effective when the installation package is launched manually in the Installer application, MacAdmins usually just build component pkgs.

The confusing part here is that both component pkgs and distribution pkgs have the same file extension. They are hard to distinguish even from the command line. To tell them apart, you can expand a pkg with the pkgutil command and look at the files in the expanded folder. Component pkgs have (among other files) a PackageInfo file and distribution pkgs have a Distribution file:

# component pkg
> pkgutil --expand component.pkg expanded_component_pkg
> ls expanded_component_pkg
Bom
Payload
Scripts
PackageInfo

# distribution pkg
> pkgutil --expand distribution.pkg expanded_distribution_pkg
> ls expanded_distribution_pkg
component.pkg
Distribution

For distribution pkgs, the Distribution file is an XML file which contains the configuration data for the package. One tag in this XML is the options tag which can have a hostArchitectures attribute. According to [Apple’s documentation on this tag](A comma-separated list of supported architecture codes), the hostArchitectures are a “comma-separated list of supported architecture codes.”

Apple documentation is a bit aged, so it gives i386, x86_64, and ppc as possible values. However, when you read the productbuild man page on macOS Big Sur you will see that arm64 is a new valid value. We will also find these extremely helpful note:

NOTE: On Apple Silicon, the macOS Installer will evaluate the product’s distribution under Rosetta 2 unless the arch key includes the arm64 architecture specifier. Some distribution properties may be evaluated differently between Rosetta 2 and native execution, such as the predicate specified by the sysctl-requirements key. If the distribution is evaluated under Rosetta 2, any package scripts inside of product will be executed with Rosetta 2 at install time.

When a distribution pkg has this attribute and it contains a value of arm64 then the installation process on an Apple silicon Mac will not check if Rosetta is installed. When arm64 is missing from the hostArchitectures, or the attribute or tag are missing entirely, the installation process on an Apple silicon Mac will asume the pkg requires Rosetta and prompt to install when necessary.

There is more good news in the next note in the man page:

NOTE: Starting on macOS 11.0 (Big Sur), productbuild will automatically specify support for both arm64 and x86_64 unless a custom value for arch is provided.

When you use productbuild to create a distribution pkg on Big Sur (Intel and Apple silicon) both arm64 and x86_64 will be added to the configuration by default.

But, when you use productbuild on Catalina or earlier, the attribute will be lacking, when means that when someone installs that pkg on an Apple silicon Mac, it will assume it requires Rosetta and prompt for installation.

Adding both architectures by default is a useful default. But can we set the value explicitly when we build the distribution pkg? And can we do so on Catalina?

Yes, you can, of course. There are even two solutions. First, instead of letting productbuild generate the Distribution xml, you can build and provide a complete Distribution xml file with the --distribution option. That will give you full, fine-grained control over all the options.

The second solution is a bit easier. You can create a requirements.plist property plist file in the form:

<?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>arch</key>
        <array>
                <string>x86_64</string>
                <string>arm64</string>
        </array>
</dict>
</plist>

Then you can provide this property list file to the productbuild command with the --product option.

> productbuild --package component.pkg --product requirements.plist distribution.pkg

This way, productbuild still generates the Distribution xml and merges in your choices from the requirements.plst. There are other options you can add which are documented in the productbuild man page.

Both of these approaches will work on Catalina as well. This way you can explicitly tell the installer system which architectures your packages will run with and not leave anything to chance.

In Whitebox Packages you can configure the hostArchitectures attribute under the “advanced options” for a distribution package.

As far as I can tell, when you install a component pkg, no checks for Rosetta are performed. Nevertheless, this is not something I would rely on. For packages that are crucial to the deployment workflow, I would recommend going the extra step and creating a distribution pkg from the component pkg with the proper flags set. This way you can ensure proper behavior.

Of course, if your package installer contains any form of Intel-only, not-universal binary, you should not abuse this just to skip the annoying Rosetta dialog, as it might lead to problems later. But, when the software you are installing is universal, you sould use this to tell the system which platforms your package supports.

Deploying the Big Sur Installer Application

When you want to provide automated workflows to upgrade to or erase-install macOS Big Sur, you can use the startosinstall tool. You can find this tool inside the “Install macOS Big Sur” application at:

/Applications/Install macOS Big Sur.app/Contents/Resources/startosinstall

Note: Apple calls the “Install macOS *” application “InstallAssistant.” I find this a useful shorthand and will use it.

Before you can use startosinstall, you need to somehow deploy the InstallAssitant on the client system. And since the “Install macOS Big Sur” application is huge (>12GB) it poses its own set of challenges.

Different management systems have different means of deploying software. If you are using Munki (or one of the management systems that has integrated Munki, like SimpleMDM or Workspace One) you can wrap the application in a dmg. Unfortunately, even though “app in a dmg” has been a means of distributing software on macOS for nearly 20 years, most management systems cannot deal with this and expect an installer package (pkg).

You can use pkgbuild to build an installer package from an application, like this:

pkgbuild --component "/Applications/Install macOS Catalina.app" InstallCatalina-10.15.7.pkg

This works for all InstallAssistants up to and including Catalina. With a Big Sur installer application this command will start working, but then fail:

% pkgbuild --component "/Applications/Install macOS Big Sur.app/" InstallBigSur20B29.pkg
pkgbuild: Adding component at /Applications/Install macOS Big Sur.app/
pkgbuild: Inferred install-location of /Applications
pkgbuild: error: Cannot write package to "InstallBigSur20B29.pkg". (The operation couldn’t be completed. File too large)

The reason for this failure is that the Big Sur installer application contains a single file Contents/SharedSupport/SharedSupport.dmg which is larger than 8GB. While a pkg file can be larger than 8GB, there are limitations in the installer package format which preclude individual files in the pkg payload to be larger than that.

When you want to distribute the “Install macOS Big Sur” application to the clients in your fleet, either to upgrade or for an erase-and-install workflow, this limitation introduces some challenges.

You can use Composer with Jamf to create a Jamf dmg style deployment, but that will only work with Jamf Pro. You could further wrap and split the application in different containers, but that will increase the creation and deployment time.

There are a number of solutions. Each with their own advantages and downsides, some supported and recommended by Apple and some… less so. Different management and deployment styles will require different solutions and approaches.

App Deployment with MDM/VPP

When you have your MDM hooked up to Apple Business Manager or Apple School Manager, you can push applications “purchased” in the “Apps and Books” area with MDM commands. This was formerly known as “VPP” (Volume Purchase Program and I will continue to use that name, because “deploy with Apps and Books from Apple Business Manager or Apple School Manager” is just unwieldly and I don’t care what Apple Marketing wants us to call it.

Since the “Install macOS Big Sur” application is available for free on the Mac App Store, you can use VPP to push it to a client from your MDM/management system.

When you do this, the client will not get the full InstallAssistant application, but a ‘stub’ InstallAssistant. This stub is small in size (20-40MB).

The additional resouces required for the actual system upgrade or installation which are GigaBytes worth of data will be loaded when they are needed. It doesn’t matter whether the process is triggered by the user after opeing the application or by using the startosinstall or createinstallmedia tool. Either workflow will trigger the download of the additional resources.

This has the advantage of being a fast initial installation of the InstallAssistant, but then the actual upgrade or re-installation process will take so much longer, because of the large extra download before the actual installation can even begin. For certain deployment workflows, this is an acceptable or maybe even desireable trade-off.

The extra download will use a Caching Server. This approach is recommended and supported by Apple.

Mac App Store and/or System Preferences

For some user-driven deployment styles, having the user download the InstallAssistant themselves can be part of the workflow. This way, the user can control the timing of the large download and make sure they are on a “good” network and the download will not interfere with video conferences or other work.

You can direct then to the Big Sur entry in the Mac App Store with a link. You cannot search for older versions of macOS Installers in the Mac App Store, but Apple has a kbase article with direct links.

You can also use a link that leads a user directly to the Software Update pane in System Preferences and prompts the user to start the download:

# Big Sur
x-apple.systempreferences:com.apple.preferences.softwareupdate?client=bau&installMajorOSBundle=com.apple.InstallAssistant.macOSBigSur

# Catalina
x-apple.systempreferences:com.apple.preferences.softwareupdate?client=bau&installMajorOSBundle=com.apple.InstallAssistant.Catalina

When the InstallAssistant is already installed, this link will open the application. When the Mac is already running a newer version of macOS or doesn’t support the version given, it will display an error.

You can use these links from a script with the open command:

open 'x-apple.systempreferences:com.apple.preferences.softwareupdate?client=bau&installMajorOSBundle=com.apple.InstallAssistant.macOSBigSur'

The downloads initiated this way will use a Caching Server. Linking to the Mac App Store is supported and recommended by Apple. The x-apple.systempreferences links are undocumented.

softwareupdate command

Catalina introduced the --fetch-full-installer option for the softwareupdate command. You can add the --full-installer-version option to get a specific version of the installer, for example 10.15.7.

You can run this command from a managed script on the clients to install the application. The download will use a Caching Server.

This would be a really useful method to automate deployment the InstallAssistant on a client, if it were reliable. However, in my experience and that of many MacAdmins, this command is very fragile and will fail in many circumstances. As of this writing, I have not been able to reliably download a Big Sur InstallAssistant with this command. Most of the time I get

Install failed with error: Update not found 

This approach is often recommended by Apple employees, however it will have to be much more reliable before I will join their recommendation.

Please, use Feedback Assistant, preferably with an AppleSeed for IT account, to communicate your experience with this tool with Apple. If this command were reliable, then it would be my recommended solution for nearly all kinds of deployments.

InstallAssistant pkg

With these solutions so far, we have actually avoided creating an installer package, because we moved the download of the InstallAssistant to the client. A caching server can help with the network load. Nevertheless for some styles of deployments, like schools and universities, using the local management infrastucture (like repositories or distribution points) has great advantages. For this, we need a package installer for the InstallAssistant.

A “magic” download link has been shared frequently in the MacAdmins Slack that downloads an installation package from an Apple URL which installs the Big Sur InstallAssistant.

This pkg from Apple avoids the file size limit for the package payload by not having the big file in the payload and then moving it in the postinstall script. Smart hack.. er… solution!

The URL is a download link from a software update catalog. You can easily find the link for the current version with the SUS Inspector tool.

But it would be really tedious to do this on every update. You, the regular reader, know the “tedious” is a trigger word for me to write a script. In this case it was less writing a script than looting one. Greg Neagle’s installinstallmacos.py had most of the pieces needed to find the InstallAssistant.pkg in the software update catalog and download it. I merely had to put the pieces together somewhat differently.

Nevertheless, I “made” a script that downloads the latest InstallAssistant.pkg for macOS Big Sur. You can then upload this pkg to your management system and distribute it like any other installation package.

It works very much like installinstallmacos.py.

./fetch-installer-pkg.py

When you start the script it will download a lot of data into a content folder in the current working directory, parse through it and determine the Big Sur Installers in the catalog. When it finds more than one installers, it will list them and you can choose one. When it finds only one Installer, it will start downloading that immediately.

You can add the --help option for some extra options (all inherited from installinstallmacos.py.

We will have to wait for the 11.1 release to be sure this actually works as expected, but I am confident we can make it work.

This approach is very likely not supported by Apple. But neither was re-packaging the InstallAssitant from disk in Catalina. This deployment method is likely closer to the supported deployment workflows than some common existing methods.

The download does not use a Caching Server, but since the goal is to obtain a pkg that you can upload to your management server, this is not a big downside.

Big Sur signature verification check

You may have noticed that when you launch the Big Sur InstallAssistant on Big Sur for the first time, it will take a long time to “think” before it actually launches. This is due to a new security feature in Big Sur that verifies the application signature and integrity on first launch. Since this is a “big” application this check takes a while. Unfortunately Big Sur shows no progress bar or other indication. This check occurs when the user double-clicks the app to open it and when you start an upgrade or installation with the startosinstall command.

There does not seem to be a way to skip or bypass this check. You can run startosinstall --usage from a script right after installing the InstallAssistant. This will do nothing really, but force the check to happen. Subsequent launches, either from Finder or with startosinstall will be immediate.

Avoiding AppleScript Security and Privacy Requests

AppleScript on macOS is a useful tool for pro users and administrators alike. Even though it probably is not (and shouldn’t be) the first tool of choice for many tasks, there are some tasks that AppleScript makes very simple. Because of this it should be a part of your ‘MacAdmin Toolbelt.’

AppleScript’s strength lies in inter-application communication. With AppleEvents (or AppleScript commands) you can often retrieve valuable information from other applications that would be difficult or even impossible, to get any other way. With AppleScript, you may even be able to create and change data in the target applications.

If you are in any way security and privacy minded this should raise your hairs. Up to macOS 10.13 High Sierra, any non-sandboxed app could use AppleScript and AppleEvents to gather all kinds of personal and private data from various script-enabled apps and services. It could even use script-enabled apps like Mail to create and send email in your name.

Since macOS Mojave, the Security and Privacy controls restricts sending and receiving AppleEvents. A given process can only send events to a different process with user approval. Users can manage the inter-application approvals in the Privacy tab of the Security & Privacy preference pane.

MacAdmins have the option of pre-approving inter-application events with a PPPC (Privacy Preferences Policy Control) configuration profile that is pushed from a DEP-enrolled or user-approved MDM.

Privacy approval

You can trigger the security approval from Terminal when you send an event from the shell to another process with osascript:

> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'

When you run this command from Terminal, you will likely get this prompt:

You will not get this prompt when you have approved or rejected the Terminal app to send events to this particular target application before. You can check the permissions granted by the user in the Automation section of Privacy tab in the Security & Privacy pane of System Preferences.

For any given source/target application combination, the prompt will only be shown once. When the user approves the privilege (“OK” button), future events will just be allowed.

When the user rejects the connection (“Don’t Allow” button), this event and future events will be rejected without further prompts. The osascript will fail and the AppleScript will return an error –1743.

> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'
79:84: execution error: Not authorized to send Apple events to Finder. (-1743)

If you want to get the approval dialogs again, you can reset the state of the source application (Terminal) with the tccutil command:

> tccutil reset AppleEvents com.apple.Terminal

This will remove the Terminal application and all target applications for it from the Automation (AppleEvents) area in the Privacy pane and show dialogs for every new request going forward. This can be very useful during testing.

Dealing with rejection

You should write your code in a ways that it fails gracefully when access is not granted. in this case osascript will return an error:

if ! osascript -e ' tell app "Finder" to return POSIX path of ((target of Finder window 1) as alias)'
then
 echo "osascript encountered an error"
 exit 1
fi

However, osascript will return errors for all kind of failures with no easy way to distinguish between them. As an example, the above will also fail when there are no Finder windows open.

If you want to distinguish AppleScript errors, you need to do so in the the AppleScript code:

if ! osascript -s o <<EndOfScript
    tell application "Finder"
        try
            set c to (count of Finder windows)
        on error message number -1743
            error "Privacy settings prevent access to Finder"
        end try
        
        if c is 0 then
            return POSIX path of (desktop as alias)
        else
            return POSIX path of ((target of Finder window 1) as alias)
        end if
    end tell
EndOfScript
then
    echo "osascript failed"
fi

Note: the -s o option of osascript makes it print AppleScript errors to standard out rather than standard error, which can be useful to find the errors in logs of management systems.

Note 2: when you are running osascript from management and installation scripts (which run as the root user) you need to run them as the current user to avoid problems.

Avoiding Privacy prompts

So, we know of one way to deal with the privacy prompts. Ideally, you would want to avoid them entirely. While this is not always possible, there are a few strategies that can work.

Don’t send to other Processes

In past versions of Mac OS X (I use this name intentionally, it’s that long ago.), scripts that showed dialogs might not display on the highest window layer. In other words, the dialog was lost behind the currently active windows. To avoid “lost” dialogs, it became best practice to send the display dialog command (and similar) to a process that had just received an activate command as well:

tell application "Finder"
    activate
    display dialog "Hello, World!"
end tell

As an alternative for Finder, the System Events process is often used as well. Jamf MacAdmins often used “Self Service.” This had the added bonus, that the dialog looks as if it comes from the Finder or Self Service, including the bouncing dock icon.

Over time, even though the underlying problem with hidden dialog has been fixed, this practice has persisted. You often even see AppleScript code use this with commands other than user interaction, where it wouldn’t have made sense in the first place. With the privacy restrictions in macOS Mojave, this practice has become actively trouble some, as you are sending the display dialog (or other) command to a separate process. The process running this script will require approval to send events to “System Events.”

osascript <<EndOfScript
    tell application "System Events"
        activate
        display dialog "Hello, World!"
    end tell
EndOfScript

In current versions of macOS, you can just use display dialog and may other commands without an enclosing tell block. Since your AppleScript code isn’t sending events to another process, no privacy approval is provided. This code has the same effect as above, but does not trigger an approval request.

osascript <<EndOfScript
    display dialog "Hello, World!"
EndOfScript

To determine whether an AppleScript command requires a tell block, you have to check where it is coming from. Many AppleScript commands that are useful to MacAdmins are contained in the ‘StandardAdditions’ scripting extension. Scripting extensions, as the name implies, extend the functionality of AppleScript without requiring their own process.

The useful commands in the Standard Additions extension include:

  • user interaction: choose file/folder/from list, display dialog/alert/notification
  • file commands: mount volume
  • clipboard commands: get the clipboard, set the clipboard to
  • sound control: set volume, get volume settings
  • system info

When your script uses only these commands, make sure they are not contained in tell blocks. This will avoid unnecessary prompts for access approval.

Exempt AppleScript commands

Some AppleScript commands are treated differently and will not trigger privacy approval:

  • activate: launch application and/or bring to front
  • open: open a file
  • open location: open a URL
  • quit: quit the application

For example, this will work without requiring approval:

osascript <<EndOfScript
    tell application "Firefox"
        open location "https://scriptingosx.com"
    end
EndOfScript

Use non-AppleScript alternatives

Sometimes, similar effects to an AppleScript can be achieved through other means. This can be difficult to figure out and implement.

As an example, I used this AppleScript command frequently for setup before Mojave:

tell application "Finder" to set desktop picture to POSIX file "/Library/Desktop Pictures/BoringBlueDesktop.png"

While Mojave was in the beta and it wasn’t really clear if or how the PPPC exemptions could be managed, I looked for a different means. I discovered Cocoa functions to read and change the desktop picture without triggering PPPC, and built a small command line tool out of that: desktoppr.

The downside of this approach is that you know have to install and/or manage a command line tool on the clients where you want to use it. There are different strategies for this, but it is extra effort compared to “just” running an AppleScript.

Build PPPC profiles to pre-approve AppleEvents

Even after you have considered the above options to avoid sending AppleEvents to another process, there will still be several situations where it is necessary. For situations where a MacAdmin needs to run a script on several dozens, hundreds, or even thousands of Macs, user-approval is simply not a feasible option.

MacAdmins can pre-approve AppleEvents (and most other privacy areas) between certain processes with a Privacy Preferences Policy Control (PPPC) configuration profile. PPPC profiles can only be managed when pushed from a user-approved or automatically enrolled MDM.

You can build such a profile manually, but it is much easier to use a tool to build these:

Your MDM solution might have a specific tool or web interface for this, consult the documentation or ask you vendor.

There is one big requirement here, though: only applications and tools that are signed with a valid Apple Developer ID can be pre-approved this way, as the signature is used to identify and verify the binary.

Determining the process that needs approval

While you can sign shell scripts and other scripts this is often not necessary. As we have seen earlier, when we ran our script from Terminal, it wasn’t the script that requested approval but the Terminal application. When your scripts run from a management system or another tool, it may not be easy to determine which process exactly needs approval.

The most practical approach to determine this, is to log the output of the ’Transparency, Consent, and Control” system (tcc) and look which process is sending the requests.

First, either use a clean test system, or reset the approvals for the processes that you suspect may be involved with tccutil.

Then open a separate Terminal window and run this command which will show a stream of log entries from the tcc process:

> log stream --debug --predicate 'subsystem == "com.apple.TCC" AND eventMessage BEGINSWITH "AttributionChain"'

There will be a lot of noise in this output.

Then run the script in question, the way you are planning to run it during deployment. If you are planning to run the script from a management system, then do that right now. You will get a lot output in the stream above.

Even when you don’t have a good idea what the parent process is going to be, you can filter the output for osascript since this is usually the intermediary tool used.

In my example I found several entries similar to this:

   0    tccd: [com.apple.TCC:access] AttributionChain: RESP:{ID: com.barebones.bbedit, PID[1179], auid: 501, euid: 501, responsible path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit', binary path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit'}, ACC:{ID: com.apple.osascript, PID[18756], auid: 501, euid: 501, binary path: '/usr/bin/osascript'}, REQ:{ID: com.apple.appleeventsd, PID[577], auid: 55, euid: 55, binary path: '/System/Library/CoreServices/appleeventsd'}

The important information here is the responsible path which give me the binary and the enclosing application that tcc considers ‘responsible.’ This is the application you need to approve.

When you are running your scripts from a management system, your MDM vendor/provider should already have documentation for this, to save you all this hassle.

With all this information, you can build the PPPC profile with one of the above tools, upload it to your MDM and push it to the clients before the deployment scripts run.

Conclusion

While the added privacy around AppleEvents is welcome, it does add several hurdles to automated administration workflows.

There are some strategies you can use to avoid AppleScripts triggering the privacy controls. When these are not sufficient, you have to build a PPPC profile to pre-approve the parent process.

macOS Version Big Sur Update

Versioning software is a surprisingly difficult task. The version number conveys technical information: how much change can you expect and might it break existing workflows and data? Users will be more willing to pay for a major upgrade with new features, so the version number is used as a marketing tool. But for developers and MacAdmins, the version number has to be as granular as possible, ideally with a different number for each build made.

Because of these, sometimes opposing, interests, it is no wonder that versioning is often a problem for MacAdmins.

A brief history of the Mac operating system versions

Before the “Mac OS” label, the Macintosh operating system was called “System 7.” “Mac OS 8”, code-named “Copland,” was supposed to be the new operating system with all the new features, but the project kept slipping and then was cancelled. The “System 7.7” update was then re-named “Mac OS 8” to get Apple out of some third party licensing deals, which were set to expire on version 8. Marketing and legal matters decided the versioning here. (But it wasn’t all just marketing: Mac OS 8 also got the new interface design that had been created for Copland.)

When Apple announced the NextSTEP based major overhaul for the Macintosh in the late nineties, they chose to not give it a different name and new version numbers. Instead they chose to label the new system as “Mac OS X”, where the “X” was read as the roman numeral ten. I assume this was a marketing choice to demonstrate continuity with the previous versions, which had been “Mac OS 8” and “Mac OS 9.”

The first version of Mac OS X had the numerical version number 10.0.0 and got four “updates” to 10.0.4 Then it got the first major “upgrade” to 10.1. This broke with conventional versioning as major upgrades should get a new major number. When “Mac OS X 10.2 Jaguar” was announced at WWDC in 2002, it was obvious that Apple now considered “Mac OS X” a brand and would stick to the 10 in the system version numbers.

With the “Xserve,” the ‘X’ became a moniker to represent Apple’s “professional” or “EXpert” hardware and distinguish them from the ‘i’ prefix used for the consumer friendly devices and software. (iMac, iBook, iPod, iTunes, iMovie, iPhoto, etc.) Later “pro” software tools, such as “Xcode,” “Xsan,” and “Xgrid” picked up that prefix. Confusingly, the leading “X” was pronounced ‘ecks’ and the trailing ‘X’ in “Mac OS X” was still pronounced as ‘ten.’ Or at least that was what Apple marketing insisted it should be called.

In 2012, Apple dropped the “Mac” from the operating system for “OS X Mountain Lion” (10.8) The “X” represented the “pro” nature of Mac platform, as opposed to “iOS” for iPhone and iPad. Apparently, the “X” was considered a stronger brand for “pro” than “Mac” at the time…

This changed when Apple finally dropped the “X-as-ten” and returned the “Mac” with “macOS Sierra” (10.12) in 2016.

Even without the “X” in the system name, the ‘10’ has remained in the version number up to macOS 10.15 Catalina.

(The “X-as-ten” lives on with “Final Cut Pro X” and “Logic Pro X”. The prefix “X” lives on with “Xcode.”)

Big Sur goes to 11

The naming and versioning of the Mac platforms operating system was largely driven by symbolism, marketing and even legal matters. The same is true this year: Apple has finally given up on the “ten” and will raise the major version of macOS to 11.

I have a post on my opinion and reaction to macOS 11. Whatever the reasons, this is the year that we will get macOS 11.

When you go to “About this Mac” on a Mac running the Big Sur beta, it will proudly announce that it is running macOS 11.0. You get the same result when you run sw_vers:

% sw_vers -productVersion
11.0

This seems easy enough, however, there is a catch to this.

10.16 vs 11.0

Big Sur will not always report 11.0 as the macOS version. It might report 10.16 in some cases. These cases are meant for situations where software might just check the second part of the version for version checks.

The rules are detailed in this post from Howard Oakley.

In Big Sur you can force the compatibility mode by setting the SYSTEM_VERSION_COMPAT environment variable to 1:

export SYSTEM_VERSION_COMPAT=1
> sw_vers -productVersion
10.16

Checking the Version

When you have a script that checked the version of macOS (or Mac OS X or OS X), so far it was safe to ignore the first number of the product version and only compare the second. For example to see if macOS was Mojave or newer, you probably would have done something like this:

minorVersion=$(sw_vers -productVersion | awk -F. '{ print $2; }')
if [[ $minorVersion -ge 14 ]]; then
  echo "Mojave or higher"
fi

Now, with the release of macOS 11.0, this setup will return 0 for the minorVersion and fail.

The obvious solution here would be to extract the majorVersion as well and compare that first:

majorVersion=$(sw_vers -productVersion | awk -F. '{ print $1; }')
minorVersion=$(sw_vers -productVersion | awk -F. '{ print $2; }')
if [[ $majorVersion -ge 11 || $minorVersion -ge 14 ]]; then
  echo "Mojave or higher"
fi

This will work well enough, even when the script runs in a setup where it might get 10.16 as the version number. But is not particularly nice to read. Also, when you want to compare update versions, these will be (presumably) the minorVersion for Big Sur and later and the third part of the version number in Catalina and earlier and things will get even more messy quickly.

Maybe there is a better way of doing this than using the product (marketing) version of macOS?

Build Version

As I mentioned earlier, the user visible version may not be granular enough for the needs of developers. And because of this macOS has a second version, called the “build version”

The build version for the current version of macOS Catalina as I am writing this is 19G2021.

> sw_vers -buildVersion
19G2021

You can also see the build version in the “About this Mac” window when you click on the version number.

The build version consists of three parts and a fourth optional one. The first number is the Darwin Version. The following capital letter designates the update. The following number (up to four digits) is the specific build number. Sometimes the build number is followed by a lower case letter.

Darwin Version

This part of the version takes its name from the Darwin core of macOS.

The Darwin Version is number that is increased on every major release of macOS. Mac OS X 10.2 Jaguar was the first release of Mac OS X to consistently report its Darwin Version as 6. From that you can conclude that 10.0 had Darwin version 4 which makes sense, because it was the fourth release of NextSTEP, the operating system Mac OS X is based on.

macOS 10.15 Catalina, has a Darwin version of 19 and Big Sur reports 20.

You can also get the Darwin version in the shell from the OSTYPE environment variable:

> echo $OSTYPE
darwin19.0

But keep in mind that environment variable may not be set depending on the context your script runs in.

A safer way to get the Darwin version is with the uname command:

> uname -r
19.6.0

This Darwin version includes the update information.

Updates

In the build version updates are tracked with a capital letter. The letter A stands for the .0 or first release of a major version. B signifies the second release or first update or .1 release, and so on.

The current Catalina build version starts with 19G so we know it is the seventh release or sixth update to Catalina (10.15.6). The current Big Sur beta starts with 20A so it is the first release or .0 version.

Build numbers

The significance of the build number is most often seen during the beta phase. While the Darwin version and update letter are fixed during the beta phase, the build number increases with every beta release. This is the most granular number we have to determine the version.

For each update and major release the build number starts over, so it can only be used to compare releases for the same major release and update version.

Traditionally, there was a difference between two- and three-digit build numbers and four-digit build numbers. The builds with lower numbers of digits were general release builds, that will run on all Macs that support this version of macOS. The four digit build numbers designated either a security update or a hardware specific build.

Hardware specific builds occur when a new Mac model is released. These usually get a hardware specific build of macOS, since the drivers necessary for the new hardware are not included in the current general release version. Even though the product version numbers of macOS are the same for the general release and the hardware specific release, they have different build numbers.

Usually, the hardware specific drivers are merged into the general release on the next update. However, until the builds are merged, MacAdmins may have to manage hardware specific system installers and workflows for new hardware. This was especially annoying with the release of the 2018 MacBook Pro which had a specific build of 10.13.6 that was never merged into the general 10.13 build. MacAdmins that wanted or needed to support 10.13 had to manage a separate installer for these MacBooks.

Intruigingly, the Big Sur beta is different: its build number started in the 4000s and switched to the 5000s with beta 3.

Special builds

Some releases of macOS have a trailing lower case letter after the build number. This is particularly common during the beta phase. It is unclear what this letter designates exactly. It might designate that the installer application was re-built one or more times.

You can use regular expressions to parse out all the individual pieces of the build version:

if [[ $buildVersion =~ ([[:digit:]]{1,2})([[:upper:]])([[:digit:]]+)(.*) ]]; then
    darwinVersion=$match[1]
    updateLetter=$match[2]
    buildNumber=$match[3]
    specialBuild=$match[4]
else
    echo "couldn't parse build version"
    exit 2
fi

But in most cases, you will not need this level of detail.

Using the Build Version

The build version provides a way to compare macOS system versions that is not subject to the whims of marketing. We can even use it distinguish hardware specific builds of macOS from general versions or determine if security or supplemental updates have been applied.

For most comparisons, we only need the Darwin version and maybe the update.

The Darwin version has had two digits since Mac OS X 10.6 Snow Leopard. It is safe to assume that you won’t be managing Macs running 10.5 Leopard any more. (And if you do, they will probably be “hand-fed” and not subject to your deployment and update automations.) Assuming a two digit Darwin version, we can use string comparison to compare build versions:

# check if Mojave or higher
if [[ $(sw_vers -buildVersion) > "18" ]]; then
...

Since all versions of Mojave start with 18A... they are all alphabetically greater than 18. The same would go if you want to check for a maximum macOS version:

# check if Mojave or earlier
if [[ $(sw_vers -buildVersion) < "19" ]]; then
...

You can also filter for specific minimum updates:

# check if Mojave 10.14.6 or later
if [[ $(sw_vers -buildVersion) > "18E” ]]; then
...

By using the build version, we are avoiding all the trouble that the “marketing-driven” build version brings with it.

zsh solution

The above works for sh, bash and zsh scripts. However, when you are using zsh, there is another useful solution. zsh provides a function to compare versions called is-at-least.

When you use zsh in the Terminal interactively, it is probably already loaded, but when you want to use it in scripts, you should use autoload to make sure it is loaded. Then you can use is-at-least this way:

autoload is-at-least
# check for Mojave or higher
if is-at-least 10.14 $(sw_vers -productVersion); then
...

Since both 11.0 and 10.16 are higher than 10.14 this will work no matter what number Big Sur might be reporting, but if you want to check that the system is Big Sur, you want to use 10.16 as the minimum, which covers both possible values:

autoload is-at-least
# check for Big Sur or higher
if is-at-least 10.16 $(sw_vers -productVersion); then
...

Conclusion

The change of the version number in macOS 11 Big Sur might affect or even break some of your system version checking in your deployment and management scripts. There are some nice and easy solutions that are more resilient to changes in the “marketing” product version.

Installomator Updated: v0.3

It’s been more than a month since the last update, and while there has been work on the dev branch, I was quite distracted with other things (like this). The good news is, that there have been quite a few contributions from others! A huge thanks to all who helped make this a better script.

All it took was for me to find some time to put all the contributions together, which I finally found some time for.

What’s new in v0.3:

  • added several new labels for total of 98
  • removed the powershell labels, since the installer is not notarized
  • when run without any arguments, the script now lists all labels
  • changed how zips are expanded because this was broken on Mojave
  • improved logging in some statements
  • several more minor improvements

Get the script and find the instructions on the GitHub repo.

Some of the contributions and requests have not yet been addressed. I believe they will require some more thinking and planning. I would like to approach those in the next version.

If you have any feedback or questions, please join us in the #installomator channel on MacAdmins Slack.

Thanks again to all those who contributed!

Use scout to read Property Lists

I have written a few posts in the past about parsing data from property list files in scripts and Terminal. My usual tool for this is PlistBuddy. However, PlistBuddy’s syntax is… well… eccentric.

Recently, Alexis Bridoux, who is also the main developer on Octory, introduced a command line tool called scout which solves many of the issues I have with PlistBuddy.

For example, you can pipe the output of another command into scout, something you can only convince PlistBuddy to do with some major shell syntax hackery.

So instead of this:

> /usr/libexec/PlistBuddy -c "print :dsAttrTypeStandard\:RealName:0" /dev/stdin <<< $(dscl -plist . read /Users/armin RealName)


With scout I can use this much clearer syntax:

> dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]"


The tool can also modify existing files, by changing, adding or deleting keys.

scout can also parse JSON and (non plist) XML files, so it can also stand in as a replacement for jq and xpath. It will also color-code output for property list, XML and JSON files.

I have been using scout interactively in the Terminal for a while now. So far, I have been refraining from using scout in scripts I use for deployment. To use a non-system tool in deployment scripts, you need to ensure the tool is deployed early in the setup process. Then you also have to write your scripts in a way that they will gracefully fail or fallback to PlistBuddy in the edge case where scout is not installed:

scout="/usr/local/bin/scout"
if [ ! -x "$scout"]; then
    echo "could not find scout, exiting..."
    exit 1
fi

realName=$( dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]" )


All of this overhead, adds extra burden to using a tool. The good news is that scout comes as a signed and notarized package installer, which minimizes deployment effort.

I wills be considering scout for future projects. If anyone at Apple is reading this: please hire Alexis and integrate scout or something like it in macOS.

About bash, zsh, sh, and dash in macOS Catalina and beyond

This is an excerpt from my book: “Moving to zsh.” At the MacAdmins Conference Campfire session I received quite a few questions regarding this, so I thought it would be helpful information. You can get a lot more detailed information on “Moving to zsh” in the book!

Calls to the POSIX sh /bin/sh in macOS are handled by /bin/bash in sh compatibility mode. You can verify this by asking sh for its version:

% sh --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
Copyright (C) 2007 Free Software Foundation, Inc.

If Apple plans to eventually remove the bash binary, they need to have a replacement which handles sh.

Enter dash

Probably not coincidentally, Apple added the dash shell in Catalina. The Debian Almquist Shell (dash) is a minimal implementation of the Posix sh standard and commonly used on other UNIX and Unix-like systems as a stand-in for sh.

Apple also added dash (but not zsh) to the Recovery system in macOS Catalina. While sh is still interpreted by bash in both the Recovery system and the regular system, this is a strong indicator that Apple eventually wants to use dash as the interpreter for sh scripts.

When your scripts which use the #!/bin/sh shebang strictly follow the POSIX sh standard, you should experience no problems when Apple switches to ‘dash-as-sh.’

Tripping over bashisms

However, there are some quirks of the current ‘bash-as-sh’ implementation in macOS that you need to be aware of. When bash stands in as sh, it will nevertheless continue to interpret ’bashisms’—language features available in bash but not sh—without errors.

For example, consider the following script shtest.sh:

#!/bin/sh

if [[ $(true) == $(true) ]]; then
  echo "still good"
else
  echo "nothing is true"
fi

This script declares the #!/bin/sh shebang and it will work fine on macOS with bash-as-sh.

% shtest.sh
still good

However, when you try to run it with zsh-as-sh or dash-as-sh, it will fail.

You can make dash interpret the script instead of bash by switching the shebang to #!/bin/dash. But macOS Catalina has another, new mechanism available. In Catalina, the symbolic link at /var/select/sh determines which shell stands in as sh. By default the link points to /bin/bash:

% readlink /var/select/sh
/bin/bash 

When you change this link to either /bin/zsh or /bin/dash, the respective other shell binary will stand in as sh.

Switch the sh stand-in to dash with:

% sudo ln -sf /bin/dash /var/select/sh

And then run the script again:

% ./shtest.sh
./shtest.sh: 3 ./shtest.sh: [[: not found
nothing is true

When interpreted with dash instead of bash, the same script will fail. This is because dash is much stricter than bash in following the sh standard. Since dash is designed as a minimal implementation of the sh standard, it has to be stricter. The double brackets [[ … ]] are a ‘bashism,’ or a feature only available in bash and other, later shells such as ksh and zsh.

Even though zsh also interprets most of these bashisms, zsh in sh compatibility mode is also stricter than bash and will error.

You can switch back to the default bash-as-sh with:

% sudo ln -sf /bin/bash /var/select/sh

Since macOS has been using bash-as-sh for a long time, there may be many such bashisms lurking in your sh scripts. You can change the above symbolic link to test your scripts with dash-as-sh.

Some common ‘bashisms’ are:

  • double square brackets [[ ... ]]
  • here documents and strings (<<< and << operators)
  • double equals operator == for tests

Shellcheck to the rescue

You can also use the shellcheck tool to detect bashisms in your sh scripts:

% shellcheck shtest.sh                                          

In shtest.sh line 3:
if [[ $(true) == $(true) ]]; then
   ^----------------------^ SC2039: In POSIX sh, [[ ]] is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC2039 -- In POSIX sh, [[ ]] is undefined.

When you change the double square brackets for single square brackets, then you get this:

% shellcheck shtest.sh

In shtest.sh line 3:
if [ "$(true)" == "$(true)" ]; then
               ^-- SC2039: In POSIX sh, == in place of = is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC2039 -- In POSIX sh, == in place of = is undefined.

Conclusion

In Catalina Apple started warning us about the eventual demise of bash from macOS. Converting your existing bash scripts and workflows to zsh, sh, or bash v5 is an important first step. But you also need to consider that the behavior of sh scripts will change when Apple replaces the sh interpreter.