A couple of months ago we had a brief discussion on an ops list at work about security of personal ssh private keys.
I've been using ssh-agent
since I was working at Demon in the late
1990s. I prefer to lock my screen rather than logging out, to maintain
context from day to day - mainly my emacs buffers and window layout.
So to be secure I need to delete the keys from the ssh-agent
when the
screen is locked.
Since 1999 I have been using Xautolock
and Xlock
with a little script
which automatically runs ssh-add -D
to delete my keys from the
ssh-agent
shortly after the screen is locked. This setup works well
and hasn't needed any changes since 2002.
But when I'm at home I'm using a Mac, and I have not had similar
automation. Having to type ssh-add -D
manually is very tiresome and
because I am lazy I don't do it as often as I should.
At the beginning of December I found out about Hammerspoon, which is a Mac OS X application that provides lots of hooks into the operating system that you can script with Lua. The Hammerspoon getting started guide provides a good intro to the kinds of things you can do with it. (Hammerspoon is a fork of an earlier program called Mjolnir, ho ho.)
It wasn't until earlier this week that I had a brainwave when I
realised that I might be able to use Hammerspoon to automatically run
ssh-add -D
at appropriate times for me. The key is the
hs.caffeinate.watcher
module, with which you can get a Lua function to run when sleep or
wake events occur.
This is almost what I want: I configure my Macs to send the displays
to sleep on a short timer and lock them soon after, and I have a hot
corner to force them to sleep immediately. Very similar to my X11
screen lock setup if I use Hammerspoon to invoke an ssh-add -D
script.
But there are other events that should also cause ssh keys to be
deleted from the agent: switching users, switching to the login
screen, and (for wasteful people) screen-saver activation. So I had a
go at hacking Hammerspoon to add the functionality I wanted, and ended
up with a pull request that adds several extra event types to
hs.caffeinate.watcher
.
At the end of this article is an example Hammerspoon script which uses these new event types. It should work with unpatched Hammerspoon as well, though only the sleep events will have any effect.
There are a few caveats.
I wanted to hook an event corresponding to when the screen is locked
after the screensaver starts or the screen sleeps, with the delay that
the user specified in the System Preferences. The
NSDistributedNotificationCenter
event "com.apple.screenIsLocked"
sounds like it ought to be what I want, but it actually gets triggered
as soon as the screensaver starts or the screen sleeps, without any
delay. So a more polished hook script will have to re-implement that
feature in a similar way to my X11 screen lock script.
I wondered if locking the screen corresponds to locking the keychain
under the hood. I experimented with
SecKeychainAddCallback
but it was not triggered when the screen locks :-( So I think it might
make sense to invoke security lock-keychain
as well as ssh-add -D
.
And I don't yet have a sensible user interface for re-adding the keys
when the screen is unlocked. I can get Terminal.app to run ssh -t
my-workstation ssh-add
but that is really quite ugly :-)
Anyway, here is a script which you can invoke from
~/.hammerspoon/init.lua
using dofile()
. You probably also want to
run hs.logger.defaultLogLevel = 'info'
.
-- hammerspoon script
-- run key security scripts when the screen is locked and unlocked
-- NOTE this requires a patched Hammerspoon for the
-- session, screen saver, and screen lock hooks.
local pow = hs.caffeinate.watcher
local log = hs.logger.new("ssh-lock")
local function ok2str(ok)
if ok then return "ok" else return "fail" end
end
local function on_pow(event)
local name = "?"
for key,val in pairs(pow) do
if event == val then name = key end
end
log.f("caffeinate event %d => %s", event, name)
if event == pow.screensDidWake
or event == pow.sessionDidBecomeActive
or event == pow.screensaverDidStop
then
log.i("awake!")
local ok, st, n = os.execute("${HOME}/bin/unlock_keys")
log.f("unlock_keys => %s %s %d", ok2str(ok), st, n)
return
end
if event == pow.screensDidSleep
or event == pow.systemWillSleep
or event == pow.systemWillPowerOff
or event == pow.sessionDidResignActive
or event == pow.screensDidLock
then
log.i("sleeping...")
local ok, st, n = os.execute("${HOME}/bin/lock_keys")
log.f("lock_keys => %s %s %d", ok2str(ok), st, n)
return
end
end
pow.new(on_pow):start()
log.i("started")