Jan 12, '04 08:16:00AM • Contributed by: jaysoffian
[NOTE: - this is part 3 of a 3-part tip. Please see part 1 for background info.]
This is the trickiest and most complicated part. It assumes you have completed parts (1) and (2) successfully. Read the rest of the hint for the detailed how-to...
- Let's create a directory where we'll put all the scripts we're about to create. I chose ~/Library/Libexec
% mkdir ~/Library/Libexec
- We're going to need some simple utilities for reading and writing to ~/.MacOSX/environment.plist. In case you're not familiar with this file, it is read as you login and used to set environment variables that are available to any application you run while you are logged in. It's an XML formatted file, and is well-formatted, so I was able to create the utilities I needed in awk:
- Create ~/Library/Libexec/getenvplist with the following contents:
#!/bin/sh set -e PATH=/usr/bin:/bin export PATH plist=$HOME/.MacOSX/environment.plist awk ' /.*/ { s = index($0, "" ) + 5 e = index($0, "") key = substr($0, s, e-s) next } /.*/ { s = index($0, "" ) + 8 e = index($0, "") string = substr($0, s, e-s) printf "%s="%s"; export %sn", key, string, key next } ' $plist - Create ~/Library/Libexec/setenvplist with the following contents:
#!/bin/sh set -e PATH=/usr/bin:/bin export PATH plist=$HOME/.MacOSX/environment.plist plist_bak="${plist}.bak" rm -f $plist_bak cp -f "$plist" "$plist_bak" awk ' (in_dict == 0) { print } // { in_dict = 1 next } /.*/ { s = index($0, "" ) + 5 e = index($0, "") key = substr($0, s, e-s) next } /.*/ { s = index($0, "" ) + 8 e = index($0, "") string = substr($0, s, e-s) keys[key] = string next } // { in_dict = 0 for (i = 2; i < ARGC; i++) { split(ARGV[i], a, "=") keys[a[1]] = a[2] } for (key in keys) { val = keys[key] if (length(val) > 0) { printf "t%sn", key printf "t%sn", val } } print next } ' $plist_bak "$@" > $plist plutil -convert xml1 $plist
- Create ~/Library/Libexec/getenvplist with the following contents:
- We're also going to need to "wrap" the default ssh-add, ssh-agent, and ssh-askpass commands:
- Create ~/Library/Libexec/ssh-add with the following contents:
#!/bin/sh DISPLAY=bogus SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass export DISPLAY SSH_ASKPASS /usr/bin/ssh-add < /dev/null
- Create ~/Library/Libexec/ssh-agent with the following contents:
#!/bin/sh eval `/usr/bin/ssh-agent -s` $HOME/Library/Libexec/setenvplist SSH_AUTH_SOCK=$SSH_AUTH_SOCK SSH_AGENT_PID=$SSH_AGENT_PID SSH_ASKPASS=$HOME/Library/Libexec/ssh-askpass CVS_RSH=ssh
- Create ~/Library/Libexec/ssh-askpass with the following contents:
#!/bin/sh exec $HOME/Library/Libexec/keychain someuser@somewhere.org sshLogin-Key
Setsomeuser@somewhere.orgto your username and machine name for now. We'll setup the key in keychain access later. - While we're at it, copy the keychain utility previously described into your Libexec directory.
- Make sure all the scripts you've just created are executable:
% chmod 755 ~/Library/Libexec/*
- Create ~/Library/Libexec/ssh-add with the following contents:
- Next, let's create our per-user Hooks directory and Login/Logout Hooks:
- Create your per-user Hooks directory:
% mkdir ~/Library/Hooks
- Create ~/Library/Hooks/LoginHook with the following contents:
#!/bin/sh PATH=/usr/bin:/bin export PATH hook=`basename $0` exec > "$HOME/Library/Logs/${hook}.log" 2>&1 LoginHook () { . $HOME/Library/Libexec/ssh-agent # give keychain time to unlock before calling ssh-add (sleep 2; $HOME/Library/Libexec/ssh-add) & } LogoutHook () { eval `$HOME/Library/Libexec/getenvplist` /usr/bin/ssh-agent -k } $hook - Create a link to this file, called LogoutHook:
% ln ~/Library/Hooks/LoginHook ~/Library/Hooks/LogoutHook
- Make sure these are executable:
chmod 755 ~/Library/Hooks/LoginHook
- Create your per-user Hooks directory:
- Almost done, fire-up Keychain Access. Select "New Password Item..." from the File menu. For Name, enter whatever you used for "someuser@somewhere.org" above. For Account: enter "sshLogin-Key". For Password, enter the passphase that protects your ssh keys.
- Quit Keychain Access and let's verify that the keychain program has access to the passphase:
% ~/Library/Libexec/keychain someuser@somewhere.org sshLogin-Key
You should get a pop-up window prompting you to grant permission to keychain to access the key. Click "Always Allow". You should see your passphrase emitted on the command line. Run keychain again and this time it should emit the passphrase without the pop-up window. Good.
Okay, that's it. You should be able to logout and log back in and you'll be able to magically ssh to places (that you've setup authorized_keys for...) w/o having to type a password. And, when you're logged out, everything is secure.
So what's going on here. Let's follow the chain of events.
- Upon login, the global com.apple.loginwindow plist is read and /Library/Hooks/LoginHook is executed as root, with the username that just logged in as its argument.
- /Library/Hooks/LoginHook then executes /Users//Hooks/LoginHook (if it exists) as the user.
- The per-user LoginHook then runs ~/Library/Libexec/ssh-agent. In the background (so as not to stall the login...) it then waits 2 seconds and calls ~/Library/Libexec/ssh-add.
- ~/Library/Libexec/ssh-agent calls the system ssh-agent. This starts up ssh-agent. Upon startup, ssh-agent emits some important environment variables. Namely, SSH_AUTH_SOCK and SSH_AGENT_PID. These are "captured" by ~/Library/Libexec/ssh-agent since it evals the output of "/usr/bin/ssh-agent -s". ~/Library/Libexec/ssh-agent then calls setenvplist to add these variables to ~/.MacOSX/environment.plist. It also adds a couple other ssh related environment variables.
- The per-user LoginHook then finishes which allows the login process to continue. loginwindow will read the updated .MacOSX/environment.plist and these environment variables will then be available to any user-run processes.
- 2 seconds or so after the per-user LoginHook was run, it runs ~/Library/Libexec/ssh-add. The delay was to give the system time to unlock the default keychain. ~/Library/Libexec/ssh-add sets up DISPLAY and SSH_ASKPASS appropriately then calls the system ssh-add. ssh-add, seeing both DISPLAY and SSH_ASKPASS set, and with stdin redirected from /dev/null, will then execute the SSH_ASKPASS program, which in this case is ~/Library/Libexec/ssh-askpass.
- ~/Library/Libexec/ssh-askpass, finally, just executes our keychain utility with the appropriate arguments. That utility will then emit the passphrase on stdout, which is "consumed" by /usr/bin/ssh-add. /usr/bin/ssh-add then unlocks your ssh keys and provides them to ssh-agent.
Now for what happens on logout:
- Upon logout, the global com.apple.loginwindow plist is read and /Library/Hooks/LogoutHook is executed as root, with the username that just logged out as its argument.
- /Library/Hooks/LogoutHook then executes /Users//Hooks/LogoutHook (if it exists) as the user.
- The per-user LogoutHook then run getenvplist. This is needed because the user has already been logged out, and the per-user environment variables are no longer available. getenvplist retrieves the variables from .MacOSX/environment.plist and makes them available (which is why the output is "eval'd"). ssh-agent-k is then run which kills the running per-user ssh-agent process.
Simple, eh? :-)
