Use the command-line to set a firmware password on macOS

For extra security, you can add a firmware password to Macs, especially since Find My Mac is essentially useless (unlike for iPads, which have an activation lock preventing thieves from reactivating the iPad after a factory reset) and DEP-to-MDM enrollments for Macs can even be avoided by thieves if they're resourceful enough.

If you have a laptop with a firmware password, you need that password to boot from anything except the startup disk. Combine that with FileVault encryption, and a stolen Mac is pretty much useless. Doesn't mean that you'll necessarily get it back, but the likelihood is higher if the device is useless to thieves.

You can, of course, enable the firmware password via Recovery Mode, but it's easier to do it from the command line:

sudo firmwarepasswd -setpasswd
You'll be prompted for the new firmware password. Afterwards, you'll need to reboot the machine for the change to take effect. (Be sure to make sure you have an actual startup disk selected in System Preferences!)

There are two modes for a firmware password: command and full. By default, the firmware password mode will be command, which means you'll be prompted for the password only if you boot from something other than the startup disk. If, for some strange reason, you want the mode to be full, it would mean you'd be prompted for a firmware password at every boot, regardless of what you're booting to.

A few other commands you might find useful...

sudo firmwarepasswd -check
checks to see if the firmware password is set.
sudo firmwarepasswd -verify
allows you to verify you have the correct password (without rebooting).
sudo firmwarepasswd -delete
deletes the firmware password. You'll need the current one to delete it, of course.

If you want to script firmware password setting, someone wrote a fairly simple script that does it. There's also firmware password manager, which is a far more sophisticated way to manage firmware passwords.

Nota Bene: If you enable a firmware password, you can get into target disk mode by holding down the Alt/Option key at boot, typing in the firmware password, and then holding down the T key. However, you will be unable to boot into Safe Mode unless you delete the firmware password.

Using an Outset boot-every script to add default applications via Munki

In Bash script to add optional installs for Munki, I introduced a script that uses PlistBuddy to add optional install items to the client machine's SelfServeManifest.

I thought at first I could use that as a boot-once script for Outset, but it seemed the script ran too early (actual first boot) and then didn't actually write the values it should.

As a workaround, I've put the script in as an Outset boot-every with a check to see if one of the optional items is already in the Munki install log. Here's an example:

#!/bin/bash

# See if this has ever run before... have to check the log, because Outset will delete the file once run. We don't want this to re-run if we update the pkg version
alreadyRun=$(cat /Library/Managed\ Installs/Logs/Install.log | grep "Firefox")

if [ -z "$alreadyRun" ]; then

# Self-serve manifest location
manifestLocation='/Library/Managed Installs/manifests/SelfServeManifest'

# PlistBuddy full path
plistBuddy='/usr/libexec/PlistBuddy'

# Add in "optional" default software
optionalDefaults=("Firefox"
"GoogleChrome"
"MSExcel2016"
"MSWord2016"
"MSPowerPoint2016"
)

# Check to see if the file exists. If it doesn't, you may have to create it with an empty array; otherwise,
if [ ! -f "$manifestLocation" ]; then
sudo "$plistBuddy" -c "Add :managed_installs array" "$manifestLocation"
fi

for packageName in "${optionalDefaults[@]}"
do
# Check it's not already in there
alreadyExists=$("$plistBuddy" -c "Print: managed_installs" "$manifestLocation" | grep "$packageName")

# Single quote expansion of variables gets messy in bash, so we're going to pre-double-quote the single-quotes on the package name
alteredPackageName="'""$packageName""'"

if [ -z "$alreadyExists" ]; then
sudo "$plistBuddy" -c "Add :managed_installs: string $alteredPackageName" "$manifestLocation"
fi
done

fi
So this basically checks for Firefox. If Firefox (one of the default optional installs) is in the install log, it won't run again.

Using Munki to rename computers based on manifest display name, user, or notes

There may be some Mac admins who don't really care that much about computer names. After all, especially if the computer isn't joined to the domain (many of our machines are not), what does it matter? It's the name that will show up in Sharing or in the Terminal.app few users use. If you do care, though, I created a Munki nopkg that looks at one of three places for a name to rename the computer name to—display_name, user, or notes.

I based this on the three custom fields that MunkiAdmin has editable for manifests.

To use it, just modify the display name, user, or notes field in the manifest you want to rename, tweak in the nopkg (via commenting/uncommenting) the installcheck_script and postinstall_script to use the field you want, and then add this nopkg as a managed_installs item to the manifest(s) you want to manage computer names for. This nopkg will change the LocalHostName, HostName, and ComputerName.

As written, this nopkg will automatically ignore AD-bound computers.

P.S. If you have a database or spreadsheet of some kind of what the computer names should be, you can also bulk-fill-in those fields. I have a simple (no subdirs on my manifests) sample script you can tweak.

Terminal command to get the full name of a Mac user

Munki-Enroll is a great little script combo that automatically changes the ClientIdentifier for Munki clients and then automatically creates a corresponding manifest on the Munki server that includes the old manifest.

I wanted to tweak it quite a bit to fit some of the quirks of how our organization does Munki client manifests, so I wrote up a tweaked version of the enroll shell script.

One of the things I wanted to get via the script is a particular user's full name, and I had trouble tracking down a tutorial on exactly how to get that. I also found that even when I used the usual instructions, on one computer, it didn't work—there was an extra carriage return before the full name... but that wasn't the case on other computers (and it wasn't a Yosemite vs. El Capitan thing either).

So this command actually gets the user's full name even if there's a random extra newline in the output. This works on both Yosemite and El Capitan (and probably earlier versions, but I haven't tested on Mavericks and before):

dscl . -read /Users/SHORTUSERNAME dsAttrTypeStandard:RealName | sed 's/RealName://g' | tr '\n' ' ' | sed 's/^ *//;s/ *$//'
where SHORTUSERNAME is the short username you're trying to get the full name of.

Basically, this read's the user's information, specifically the RealName. Then it strips out the RealName: part, then strips out any newline indicators, then finally strips out any preceding or trailing spaces.

Not sure if anyone else out there is looking for how to get the full name of a user using the terminal on a Mac, but that's how you do it.

Putting Dockutil add/removes in arrays in Bash

Most of the time when you see scripts using Dockutil to add and/or remove items from the Dock in Mac OS X, they look like this:

/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP1.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP2.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP3.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP4.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP5.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP6.app' --no-restart
/usr/local/bin/dockutil --add '/Applications/NAMEOFAPP7.app' --no-restart
That's totally fine to do, but I figured putting the additions and removals into arrays may be a cleaner way to manage things, so I wrote up a sample script for that.

There is an array of Dock items to remove and then an array of Dock items to add. Then it loops through all the removal items, makes sure each item already exists (no point in removing something that doesn't exist), and then removes it. Then the script loops through all the addition items, makes sure each item isn't already there (otherwise, Dockutil warns that you should using the --replacing option) and that the source item exists (otherwise, the non-existent item gets added as a question mark), and then adds it.

Condition Scripts for Munki

If you've been using Munki for a while, you probably already know about conditional items that allow you add items to optional installs, managed installs, etc. based on a condition like whether the client is a laptop or a desktop, or whether the client is running 10.10 or 10.11.

But you can also create your own admin-provided conditions.

If you write your own script, it should go in /usr/local/munki/conditions on the client machines. At the end of the script, your script should write to the /Library/Managed Installs/ConditionalItems.plist file. For bash scripts, that would be just using a defaults write command. For Python scripts, you would want to read the existing .plist keys/values, and then write back with your additions.

If you want to test your script, put in a condition you know to be true and then run

sudo /usr/local/munki/managedsoftwareupdate -vvv
on a test client. You should see, for that relevant item, that the condition evaluated to true. Then try again for a false condition.

Don't panic if you write your script, run Munki or Managed Software Center, and see the ConditionalItems.plist file appear in /Library/Managed Installs and then disappear very quickly afterwards. That's normal behavior.

Tim Sutton has a collection of Munki condition scripts. I also wrote one that checks ping time back to the Munki server.

Creating a Munki nopkg for Remote Management

When are nopkg items in Munki appropriate?

Greg Neagle (author of and primary developer on Munki) makes a good point that in most cases, a real .pkg (instead of a nopkg) is the best choice, because of versioning and receipts.

One thing that nopkg items in Munki are good for, though, is checking to make sure a setting stays. If you want to "install" a setting on a user's Mac and you use a .pkg to do so, the only thing Munki is going to check is that the receipt is there. So if the user changes the setting afterwards, the Munki client will never know the setting reverted.

If, however, you use a nopkg, any reverted setting will switch back when the client checks back in with the Munki server (roughly every hour by default).

The final nopkg and lessons learned along the way

I wanted to put together a System Settings > Sharing > Remote Management nopkg, but I had do a lot of Googling and trial and error to get a working version of the nopkg. Here are a few things I learned along the way:

  • ps aux | grep "[A]RDAgent.app" seems to be the most reliable way to check whether Remote Management is enabled, as far as my own testing and based on commands I've found through Google searches. Unfortunately, it isn't always reliable. I've done significant testing and sometimes if you uncheck the Remote Management box in System Preferences > Sharing, the ARDAgent.app will still be "running."
  • Both dscl . -list /Users dsAttrTypeNative:naprivs and dscl . -list /Users naprivs list which users are listed as Remote Management–enabled users. For the eventual script, I went with the latter, since it's easier to type and remember.
  • If you want only specific users, it is not enough to run sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -configure -allowAccessFor -specifiedUsers. For example, if there are two users in naprivs and then you specify only one, both will still be in there. Even if you disable Remote Management and then specify only one, both of the old ones will pop back in there, so you do actually have to run sudo dscl . delete /Users/username naprivs on each username that's in there before you give privileges to the one or two users you want instead of the previously existing one.
  • Likewise, enabling Remote Management for all users is completely independent of the specific users. So if you switch to all users and then switch back to specific users, OS X will still look for users with naprivs.

Acknowledgements

I got a lot of help from rtrouton's remote management script for the ARD_AllLocalUsers setting, and from haircut's remote management script for the loop on removing users in the naprivs group, as well as Apple' official documentation on Remote Management.

How to read from and write to .plist files using Python

Python and Macs

Python is a popular tool among Mac Admins. You can find a fairly comprehensive (and long) list of Python-based Mac Admin tools at Python Macadmin Tools, so it's a handy thing to be able to throw together a Python script every now and then.

.plist files

A lot of settings in Mac OS X are managed in property lists (or .plist files), so it's also a handy thing to be able to use a Python script to manipulate .plist files.

.plist files and Bash (Bourne Again Shell)

Generally, if you're using bash (the default shell when you open the Terminal.app), you would read from and write to .plist files using a defaults command. For example, if you wanted to see whether the last user logged in (loggedIn) is still logged in or if no one is logged in (loggedOut or Restart), you would use a command like this:

defaults read /Library/Preferences/com.apple.loginwindow lastUser
or if you wanted to show hidden files, you would change the relevant .plist using a command like this:
defaults write com.apple.Finder AppleShowAllFiles -bool TRUE

.plist files and Python... and plistlib

This is where things get a bit tricky, because there is no equivalent to defaults in Python. Python has a module you can import called plistlib that presumably lets you read from and write to .plist files.

If you follow the examples in the documentation, though, you may run into some errors.

For example, if you paste in the code on how to generate a .plist (even if you import datetime, plistlib, and time), you'll get an error of

NameError: name 'fileName' is not defined
and then if you actually define fileName with the path to a file, you'll get
NameError: name 'dump' is not defined
Now I get that probably dump needs to be imported from some other module, but seriously in documentation you need to have code that people can copy and paste and see the results of and tweak, instead of having code that's basically useless.

That's why I'm writing this guide, because it's difficult to find straightforward documentation on how to actually use plistlib.

This is an actual basic script that will actually write a .plist based on a dictionary you define:

#!/usr/bin/python

import os
import plistlib

def main():
   pl = {
   "aString" : "Doodah",
   "aList" : ["A", "B", 12, 32.1, [1, 2, 3]],
   "aFloat" : 0.1,
   "anInt" : 730
   }

   fileName=os.path.expanduser('~/Desktop/example.plist')

   plistlib.writePlist(pl, fileName)

if __name__ == '__main__':
   main()
and if you want to read a .plist, this is actual real code that will really work (obviously the script above and script below are just examples, and you would tweak them to fit your workflow):
#!/usr/bin/python

import os
import plistlib

def main():

   fileName=os.path.expanduser('~/Desktop/example.plist')
   
   if os.path.exists(fileName):

      pl=plistlib.readPlist(fileName)
   
      print '\nThe plist full contents is %s\n' % pl

      if 'aString' in pl:
         print 'The aString value is %s\n' % pl['aString']
      else:
         print 'There is no aString in the plist\n'

   else:
      print '%s does not exist, so can\'t be read' % fileName

if __name__ == '__main__':
   main()
You may run into an issue, though, with some .plist files, and you'll get an error message like this:
Traceback (most recent call last):
File "./NAMEOFYOURSCRIPT.py", line 26, in
main()
File "./NAMEOFYOURSCRIPT.py", line 13, in main
pl=plistlib.readPlist(fileName)
File
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plistlib.py", line 78, in readPlist
rootObject = p.parse(pathOrFile)
File
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plistlib.py", line 406, in parse
parser.ParseFile(fileobj)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 8
I believe this stems from some .plist files being XML and others being binary. You can revert to bash and plutil to convert from binary to XML, but extra-converting a file in bash to be able to read from it in Python isn't ideal.

.plist files and Python... and FoundationPlist

Fortunately, there's a solution for this. FoundationPlist (by Greg Neagle, author of Munki) can read from and write to both XML and binary .plist files reliably.

To use FoundationPlist, just put it and its corresponding __init__.py in a FoundationPlist subfolder of your Python script, and then put

import FoundationPlist
in your Python script.

You can see a good example of this subfolder setup in Outset's code.

Once the FoundationPlist module is imported, you can use it similarly to how you would use plistlib:

munki_prefs_location='/Library/Preferences/ManagedInstalls.plist'
munki_prefs=FoundationPlist.readPlist(munki_prefs_location)
manifest=munki_prefs['ClientIdentifier']
print 'The client identifier is %s' % manifest
This is by no means a comprehensive guide to manipulating .plist files using Python—it's just a start, because I couldn't find good, usable documentation on the basics (read a .plist, write to a .plist).

PlistBuddy (not Python)

If you don't need to use Python and the defaults command isn't cutting it for you, Macs also come with a handy built-in command-line tool called PlistBuddy that can manipulate both binary and XML .plist files. I have several tutorials on how to use PlistBuddy.