Previous articles in this series:
When you place a text file named PolicyBanner
in the /Library/Security
directory, macOS will display this file before the Login Window. The user will have to accept the banner before they can log in.
- Apple Support: About Policy Banners in macOS
The PolicyBanner file can be plain or rich text (txt, rtf, or rtfd file extensions). You can find a very simple PolicyBanner.rtf
in the sample files, or create or provide your own.
The support article notes that the PolicyBanner file needs to be readable by every user in order to be displayed.
To build a package that installs your policy banner file, create a new project folder with a payload subdirectory:
> mkdir -p PolicyBanner/payload/Library/Security
> cd PolicyBanner
Then copy the PolicyBanner file to the payload directory, and ensure that the read mode is enabled:
> cp /path/to/PolicyBanner.rtf payload/Library/Security
> chmod 644 payload/Library/Security/PolicyBanner.rtf
Then create a new buildPolicyBannerPkg.sh
script file in your favored text editor:
#!/bin/sh
pkgname="PolicyBanner"
version="1.0"
install_location="/"
identifier="com.example.${pkgname}"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
projectfolder=$(dirname "$0")
# recursively clear all extended attributes
xattr -cr "${payloadfolder}"
# ensure banner file is world readable
chmod 644 "${payloadfolder}/Library/Security/PolicyBanner.rtf"
# build the component
pkgbuild --root "${payloadfolder}/payload" \
--identifier "${identifier}" \
--version "${version}" \
--install-location "${install_location}" \
"${projectfolder}/${pkgname}-${version}.pkg"
This script is very similar to the buildBoringWallpaperPkg.sh
script from the previous post. You could easily copy that script and modify the pkgname
variable, and add the lines that ensure the correct file mode.
Your folder structure should look like this:
📁 PolicyBanner-1.0
⚙️ buildPolicyBannerPkg.sh
📁 payload
📁 Library
📁 Security
📄 PolicyBanner.rtf
When you run the build script it will generate a package named PolicyBanner-1.0.pkg
. Inspect the package with pkgutil
or Suspicious Package and verify that it contains the PolicyBanner.rtf
file as its payload with the correct install location.
You should always verify your self-built packages with an inspection tool after building and before the first test installation. This step can quickly catch several frequent errors.
Once you have inspected the pkg file to your satisfaction, you can install it on a test client. After running the installation, verify that you can find the PolicyBanner file in the /Library/Security
folder and then logout to see if it works.
While the use cases for this kind of simple policy display are limited, this example demonstrates how system administrators use pkg installers to modify settings and behavior in macOS.
Uninstall Policy Banner
In the previous post, we said it makes sense to build an uninstall script alongside the package itself. To uninstall this pkg, you can use the following script:
#!/bin/sh
# uninstall Policy Banner
# reverts the installation of com.example.PolicyBanner
# check for root
if [ "$(whoami)" != "root" ]; then
echo "requires root privileges..."
exit 1
fi
# remove the file
rm -v "/Library/Security/PolicyBanner.rtf"
# forget the pkg receipt
pkgutil --forget com.example.PolicyBanner
A Simple Postinstall Script
Apple’s support article on policy banners mentions:
If the policy banner still doesn’t appear, update the Preboot volume:
diskutil apfs updatePreboot /
To be honest, I have never (so far) encountered this problem and had to apply this fix, but, for the sake of example, we will be extra paranoid… er… thorough and apply this command after installation, just to be sure.
macOS installation packages allow for scripts or binaries to run before or after the payload is laid down on the target volume. We will go into much more detail later. For now, we will create a postinstall
script which runs the command above and add it to the package.
In the PolicyBanner
project folder, create a new sub-directory called scripts on the same level as the payload
directory.
> cd PolicyBanner
> mkdir scripts
Then create a script file named postinstall
(no file extension!) in the scripts
directory with the following code:
#!/bin/sh
## run update preboot
# extra paranoid interpretation of
# https://support.apple.com/en-us/119845
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
# only run when installing on System Volume
if [ "$3" != "/" ]; then
echo "Not installing on /, exiting"
exit 0
fi
echo "running updatePreboot"
diskutil apfs updatePreboot /
After creating the file, ensure its executable bit is set:
> chmod +x scripts/postinstall
Your PolicyBanner project folder should look like this:
⚙️ buildPolicyBannerPkg.sh
📁 payload
📁 Library
📁 Security
📄 PolicyBanner.rtf
📁 scripts
⚙️ postinstall
⚙️ uninstallPolicyBanner.sh
The diskutil
man page mentions that you might break login when running the updatePreboot
command against a user database that does not match the system, so we are going to avoid doing that.
The script checks if the third argument $3 matches “/” and exits the script when it does not.
The installation system passes the target volume as the third argument $3, so this check ensures the postinstall will only run when the banner is installed on the current system volume.
Then, having passed that check, it will run the command. There are a few echo commands whose output will appear in the installation log. These are helpful to see what is going on.
We still have to instruct pkgbuild
to include the postinstall
script in the package file. Open buildPolicyBannerPkg.sh
and modify it like this:
#!/bin/sh
pkgname="PolicyBanner"
version="2.0"
install_location="/"
identifier="com.example.${pkgname}"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
projectfolder=$(dirname "$0")
payloadfolder="${projectfolder}/payload"
scriptsfolder="${projectfolder}/scripts"
# recursively clear all extended attributes
xattr -cr "${payloadfolder}"
xattr -cr "${scriptsfolder}"
# ensure banner file is world readable
chmod 644 "payload/Library/Security/PolicyBanner.rtf"
# ensure postinstall is executable
chmod 755 "${scriptsfolder}/postinstall"
# build the component
pkgbuild --root "${payloadfolder}" \
--identifier "${identifier}" \
--version "${version}" \
--install-location "${install_location}" \
--scripts "${scriptsfolder}" \
"${projectfolder}/${pkgname}-${version}.pkg"
First, update the version of the package. You should update the package’s version every time you update its contents. This allows the installation system to distinguish a re-application of the same package from an installation of a different version.
Then we create a variable referencing the scripts folder, run the xattr
command to clear extended attributes from its contents and ensure the executable bit is set on the postinstall
.
Finally, we add a --scripts
option referencing the scripts folder to the pkgbuild
command. Take note of the trailing backslash \
in that line, that allows the command to continue to the next line. Without the backslash, the command will error.
Run the buildPolicyBannerPkg.sh
script. This will create a pkg file named PolicyBanner-2.0.pkg
file in the project folder. When you expand this package file with pkgutil
, you will see a sub-directory named Scripts
which contains the postinstall
.
> pkgutil --expand PolicyBanner-2.0.pkg PolicyBanner-2.0-expanded
📁 PolicyBanner-2.0-expanded
📄 Bom
📄 PackageInfo
📄 Payload
📁 Scripts
⚙️ postinstall
Installation Log
Install the package file on a test Mac using the Installer.app. When the installation has completed successfully, choose “Installer Log” (command-L) from the “Window” menu and then choose “Show All Logs” (command-3) from the “Detail Level” popup in the log window.
The Installer log is always quite detailed or even noisy. Since we know we are looking for log entries regarding the postinstall
script, you can enter ‘postinstall’ in the search field of the log window. This filters down the log to the entries relevant to the postinstall
script:
installd[690]: PackageKit (package_script_service): Preparing to execute script "./postinstall" in /private/tmp/PKInstallSandbox.1gFziD/Scripts/com.example.PolicyBanner.rQLtIr
package_script_service[1168]: PackageKit: Preparing to execute script "postinstall" in /tmp/PKInstallSandbox.1gFziD/Scripts/com.example.PolicyBanner.rQLtIr
package_script_service[1168]: Set responsibility to pid: 13061, responsible_path: /System/Library/CoreServices/Installer.app/Contents/MacOS/Installer
package_script_service[1168]: PackageKit: Executing script "postinstall" in /tmp/PKInstallSandbox.1gFziD/Scripts/com.example.PolicyBanner.rQLtIr
package_script_service[1168]: ./postinstall: running updatePreboot
package_script_service[1168]: ./postinstall: Started APFS operation
package_script_service[1168]: ./postinstall: UpdatePreboot: Commencing operation to update the Preboot Volume for Target Volume disk3s1 (Macintosh HD)
package_script_service[1168]: ./postinstall: UpdatePreboot: Commanded forwarding to System-role regardless of target input = InhibitAutoGroupTarget = 0; ForwardingEnabled
(I have removed some columns and text for space and clarification. The process numbers will be different in your log.)
If you do not see entries for the
postinstall
script in the log, you have made an error configuring the package. Most likely errors are that you named thepostinstall
script wrong (usually by accidentally adding a .sh or .txt file extension) or did not set the executable bit correctly.
First, we see a few entries where the installer system is preparing the postinstall
script to run, then we see a line:
./postinstall: running updatePreboot
This is the output of the echo
command in our postinstall
script. Here we can tell that the script passed the system volume check successfully and will run the diskutil
command next.
Then we see a lot more lines which are the output from the diskutil
command itself. The updatePreboot
verb is very verbose, which can actually be helpful when diagnosing problems.
You can also find this output in /var/log/install.log
. macOS will append all installations to this log file. That includes regular runs of the software update system, so the install.log
will get quite big and noisy over time. When you are debugging package installation issues it is very useful to note the time of your installation, so that you can narrow down the area of the log file you need to inspect.
This has been a very simple example for an installation script. We will re-visit this topic in more detail in a later post.