.@ Tony Finch – blog


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")