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
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 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
How to Retrieve the Password in the Script
To retrieve a password from the keychain in a script, use the
> 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"