Another Simple Package: Policy Banner

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.

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 the postinstall 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.

Published by

ab

Mac Admin, Consultant, and Author

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.