Important Correction: 12 Jan, 2024

In the initial version of this blog, I described a method to pass the secret to the security command using a subshell. This is not secure, as the secret is exposed to process exploration tools such as ps. I have updated the blog to use a custom tool written in go to pass the secret. I wish to thank my respected peer at Recurse Center, Jacob Vosmaer, for pointing this issue out.


Goals

For one of my current projects, I’m trying to create a secure way to store and access secrets such as API keys on my Mac, such that they can be accessed from bash scripts. I’m using the MacOS Keychain to store these secrets, so I decided to write about it because it is generally useful. Ideally, I would use a yubikey or something similar to store and access these secrets, but this alternative is an interesting option, and it balances security and convenience.

Keychain Settings Hardening for Mac

Before using this process I hardened the default settings for my Keychain. I want to make sure that the Keychain locks after a period of inactivity. To do this, I followed the steps below:

  1. Open Keychain Access:

    • Open the Keychain Access application on your Mac. You can find it in the Utilities folder within your Applications folder, or you can use Spotlight to search for it.
  2. Adjust Auto-Lock Settings:

    • In Keychain Access, select the “login” keychain from the list on the left side.
    • Go to the menu bar and choose Edit > Change Settings for Keychain “login”.
    • Here, you can set the Keychain to lock after a certain period of inactivity. Check the “Lock after” checkbox and enter the desired number of minutes.
    • You can also choose to lock the Keychain when your computer goes to sleep by selecting the “Lock when sleeping” checkbox.
    • After adjusting these settings, click Save.

When these settings are in place, and your Keychain locks, accessing it again should prompt for your Mac login password. This is preferable to having the Keychain unlocked all the time.

Require Touch ID?

Unfortunately, I haven’t found a way to require Touch ID for every use of the Keychain. I might revisit this later and write an update after contacting Apple Support. This would be an excellent feature to add to the Keychain, biometric authentication for every use on some chains? If that is not a planned feature, I will revisit this and write an update that uses a yubikey or something similar.

Lets Get Started

Storing And Retrieving Secrets From Keychain

Apple provides a command-line interface to create and retrieve secrets in macOS Keychain, this is especially handy for bash scripts. It is under the security command as two subcommands add-generic-password and find-generic-password.


To Create A New Secret Item

To securely store a new secret item in the keychain, normally we would use the command security add-generic-password. Like this:

security add-generic-password -U -a "MyAPIUser" -s "MyAPIService" -w "thepassword"

Where:

  • security add-generic-password: Adds a password to the Keychain.
  • -a "MyAPIUser": Sets the service user name (here, “MyAPIUser”).
  • -s "MyAPIService": Sets the service name (here, “MyAPIService”).
  • -w "thepassword": Sets the password (here, “thepassword”).
  • -U: Updates the item if it already exists in the keystore.

But this is where things got inconvenient for me. In fact, I struggled to find a way to pass the secret to the security command without exposing it in the terminal history or environment.

I tried using a subshell to pass the value to -w, and I tried using a temporary file and redirection. But, I couldn’t get either to work in a way that didn’t expose the secret or just fail to be read by the security command as intended.

After the error was spotted in my initial solution, I paused, and took a step back. I realized that I could write a tool to do this. Its not as elegant as pure shell scripting, but it works. I have included the code as a gist at the end of this blog. To use the tool, you simply compile it and pass the secret inside of a file.

Here’s an example of running it:

glitch@cafe% printf "a secret" > secret.txt # don't do this in real life
glitch@cafe% ./keychain-tool -account "MyAPIUser" -service "MyAPIService" -file secret.txt
Duplicate item found. Updating existing item...

To Retrieve The New Secret Item

When retrieving the secret, it’s crucial not to print it to the console. Instead, you should pipe the output directly to the command that requires the secret. Or pass it via an environment variable. However, make sure to unset the environment variable after use. Here is an example of retrieving the value of the secret that we stored above:

Here’s an example:

security find-generic-password -a "MyAPIUser" -s "MyAPIService" -w | <command_that_needs_the_secret>

Where:

  • security find-generic-password: This is a command to retrieve a stored password from the macOS Keychain.
  • -a "MyAPIUser": Specifies the account name associated with the password.
  • -s "MyAPIService": Indicates the service name for which the password is stored.
  • -w: Directs the command to output only the password itself, not the entire Keychain item object.
  • |: This is a pipe symbol that passes the output of the preceding command as input to another command.
  • <command_that_needs_the_secret>: Replace this with the command that needs the API key. The key is passed directly to this command without being printed or stored in a temporary variable.

This approach ensures the API key remains secure and is not exposed in the terminal history or environment.


A Practical Example (SSH Key)

This example involves storing and retrieving an ssh private key from the keystore. The key is stored in the Keychain using our custom tool keychain-tool, and encoded in base64 to avoid issues with special characters.

When needed, it’s retrieved, decoded, and passed to ssh-add via a temporary file. The temporary file is created with mktemp and securely overwritten with random data after use. Clearly this is not the most secure method, but I think it is better than persistently storing the key in a file on disk. A much better method would be to use a yubikey or something similar, so that the key is completely stored in hardware.

To Store The SSH Key

glitch@cafe% cd ~/.ssh/
glitch@cafe% base64 -i the-key.pem > sshkey.b64 # base64 encoded ssh key
glitch@cafe% ./keychain-tool -account "ec2user" -service "MySSHHost" -file sshkey.b64
glitch@cafe% dd if=/dev/urandom of=sshkey.b64 bs=1 count=$(stat -f%z sshkey.b64)
glitch@cafe% rm sshkey.b64

Note: The dd command is used to securely overwrite the temporary file with random data. This is done to prevent the secret from being recovered from the disk.

  • if=/dev/urandom: Specifies the input file as /dev/urandom, which generates pseudo-random bytes.
  • of=sshkey.txt: Specifies the output file where the random data will be written.
  • bs=1: Sets the block size to 1 byte. This means dd will read and write up to 1 byte at a time.
  • count=$(stat -f%z sshkey.txt): Uses the stat command to get the size of sshkey.txt in bytes and sets this as the count. This tells dd to write only as many bytes as the size of sshkey.txt.

The purpose of this command is to overwrite the file sshkey.txt with random data, effectively making the original content unrecoverable. This is often done for security reasons to securely erase sensitive data.

To Retrieve The SSH Key And Use It

# Create a secure temporary file in the home directory
tmpfile=$(mktemp ~/sshkey.XXXXXX)
chmod 600 "$tmpfile"

# Retrieve the SSH key from Keychain and decode it into the temp file
security find-generic-password -a "ec2user" -s "MySSHHost" -w | base64 --decode > "$tmpfile"

# Load the SSH key into ssh-agent
ssh-add "$tmpfile" # ssh-agent requires a file with restricted permissions, so we can't use and fd/pipe

# Securely overwrite the temporary file with random data
dd if=/dev/urandom of="$tmpfile" bs=1 count=$(stat -f%z "$tmpfile")

# Remove the temporary file
rm "$tmpfile"

To Delete The SSH Key From The Keychain

You may need to remove the secret. To do so, you can use the security delete-generic-password command. This command requires the account name and service name, which you can find using the security find-generic-password command. Heres an example using out ssh key from above:

security delete-generic-password -a "ec2user" -s "MySSHHost"

Conclusion

I hope this helps someone. I’m going to try to write and make more this year. Sending warmth and kindness to my reader. As always, I hope you are well.


Appendix

Keychain Tool

Tool Code
To Compile
go mod init example.com/m/v2
go mod tidy
go build -o keychain-tool