pizauth: dump and restore

Blog archive

Recent posts
Some Reflections on Writing Unix Daemons
Faster Shell Startup With Shell Switching
Choosing What To Read
Debugging A Failing Hotkey
How Often Should We Sharpen Our Tools?
Four Kinds of Optimisation
Minor Advances in Knowledge Are Still a Worthwhile Goal
How Hard is it to Adapt a Memory Allocator to CHERI?
"Programming" and "Programmers" Mean Different Things to Different People
pizauth: First Stable Release

It’s been a little while since I’ve written about pizauth. I had expected to make a 1.0.0 release by now: that release will be where I commit, to the extent reasonably possible, to maintaining a stable interface in the future. However, there was one feature I couldn’t find a good design for: persisting state to disk. This missing feature had been nagging away at me (and a small, though growing, number of other people) for quite a while: recently I finally alighted upon a design that I thought would work well. pizauth-0.2.2 is the first release to support this. Perhaps this will be the last release before 1.0.0!

A little backstory might be interesting. pizauth allows you to authenticate yourself to a remote OAuth2 server (generally using a username, password, and two-factor authentication) and then receive back access tokens that you can use to access services without further authentication. For example, if you’re using an email client on your computer, you may well be using OAuth2 to read and send email, even without realising it. OAuth2 has other uses too: for example, I can (and do!) use OAuth2 to see how long my washing machine has left to complete!

Access tokens typically have a lifetime of about an hour, at which point they become useless. Since you probably don’t want to be forced to fully reauthenticate every hour, OAuth2 defines a second concept called refresh tokens which allow you to obtain new access tokens. pizauth waits until just before an access token is about to expire before using an account’s refresh token to obtain a new access token, giving the illusion of continually valid access tokens. Refresh tokens don’t have a fixed lifetime, and typically remain valid at least for many days and potentially indefinitely.

While we obviously don’t ever want someone to get hold of any secrets we possess, the short lifetime of an access token gives successful attackers a limited window of opportunity in which to impersonate you. In contrast, if an attacker gets hold of a refresh token, they may be able to impersonate you almost indefinitely. I think of refresh tokens as being “superuser” tokens, since if someone gets hold of the superuser password of your server, there is almost no limit to the bad things they can do.

Because of the sensitivity of refresh tokens, I made a decision early on that pizauth would keep all state about tokens (access and refresh) in memory and not save them on disk. That decision is the fundamental reason why pizauth is a daemon, and not a “reactive” system that loads tokens each time it is run. Although storing secrets in memory isn’t totally fool-proof [1], storing secrets on disk is fraught with risk.

Mostly, having pizauth work solely in memory works well. However, there is one common annoyance: whenever you reboot a machine and reload pizauth, you have to reauthenticate all your accounts. If you only have one account and reboot every few weeks, this isn’t much of a worry. If you have to reboot near-daily and have multiple accounts, it becomes a chore.

The obvious answer is for pizauth to persist access and refresh tokens to disk so that, after a reboot, it can reload them. This sounds simple, and most of the other OAuth2 Unix tools I know of support it — so why might I say that I found it hard to find a satisfying design for this feature in pizauth?

Let’s start by stating the obvious. We really – really, really! – want to encrypt refresh tokens in a way that other programs can’t decrypt without interaction from the user. pizauth could define its own encryption strategy, but I think most users would prefer to use an external encryption tool that they already trust. Although gpg is still probably the best known encryption tool on Unix, for the rest of this post I’m going to assume the encryption tool we’re using is Age, because I find it much easier to use.

I use a password with Age [2], so if I want to use Age with pizauth, I somehow need to type that passsword into Age in order to decrypt my secrets. Personally I type that password in at the terminal, so it wouldn’t be that difficult to have pizauth call Age and block until I’ve typed my password into Age. However, many people quite reasonably use password or encryption tools which don’t expect to be called by a program like pizauth: some expect to call a program like pizauth, and some expect the user to manually copy and paste output over to their program of choice. Neither use case would be well served by pizauth calling an external tool.

One of the challenging things about design issues in general is that existing solutions tend to narrow our thinking. This was exactly what happened to me: I found it hard to think beyond “having pizauth call encryption tools is a bad idea but current OAuth2 tools do just that”. And so, despite knowing that some people wanted this feature, I couldn’t bring myself to implement it in a way I thought I’d come to regret.

Eventually the way out of my bind became clear to me: rather than having pizauth call an encryption tool, I needed to allow encryption tools to call pizauth. Doing so would allow users to do whatever weird and whacky thing they might want to do to get refresh tokens into pizauth.

Once I’d made this (in retrospect rather simple) observation, a reasonable design soon occurred to me. First I needed a way for pizauth to persist “state” (the name I’ve given to a combination of access and refresh tokens, as well as parts of pizauth’s configuration). For that, I added a command-line call pizauth dump which causes pizauth to output its state to stdout. Second I needed a way for pizauth to load that state back in. For that, I added pizauth restore which reads a previous dump from stdin. A user can then do things like:

$ pizauth show act
54Z7JXDAAQZNdYB8qIP4NJhlA3SVdbCd
$ pizauth dump | age -e -o pizauth.age -R age_public_key
$ pizauth shutdown
$ pizauth server
$ pizauth show act
ERROR - Access token unavailable until authorised with URL ...
$ age -d -i age_private_key -o - pizauth.age | pizauth restore
Enter passphrase for identity file "age_private_key":
$ pizauth show act
54Z7JXDAAQZNdYB8qIP4NJhlA3SVdbCd

Running through each command in order:

  1. pizauth show shows that pizauth has an access token for account act;

  2. pizauth dump writes pizauth’s state (including that access token and, hopefully, a refresh token) to stdout which is immediately encrypted by Age (age -e) and written to disk;

  3. pizauth shutdown shuts pizauth down;

  4. pizauth server restarts pizauth,

  5. but since no authentication has yet occurred pizauth show act cannot show an access token;

  6. the previously dumped state can be decrypted with Age (age -d) and piped back into pizauth with pizauth refresh

  7. an access token is now available, even though I haven’t reauthenticated.

The advantage of this scheme, as I hope this example makes clear, is that the user is in control of encryption and (in particular) decryption: I think (hope!) this scheme is flexible enough for any conceivable decryption tool the user might want to use.

Astute readers will probably be confused at this point: allowing users to manually dump and restore pizauth’s state is all well and good, but how should one know when to dump state? Both access and refresh tokens can change over time, so if you want an accurate restore, you need an up to date dump.

One possibility is simply to regularly poll pizauth (perhaps every minute). A nicer way is to make use of the fact that, in general, encryption doesn’t require user interaction. pizauth has thus grown a new global option token_event_cmd which allows an arbitrary command to be run every time an access or refresh token changes (e.g. a new token, a refreshed token, or a token invalidated) in some way [3]

token_event_cmd =
  "pizauth dump | age -e -o pizauth.age -R age_public_key";

In this example, every time a token changes, output from pizauth dump is immediately encrypted and written to disk. In a startup shell script I run when I log in I then have:

while true do
  age -decrypt -i age_private_key -o - pizauth.age \
    | pizauth restore \
    && break
done

The while loop deals with the fact that I sometimes enter my password incorrectly: this will keep calling age until I get my password correct!

pizauth has one final trick up its sleeve — or, rather, a check up its sleeve. When you reload pizauth’s configuration, any changes in an account’s “secure” details (i.e. those settings that, if changed, could cause you leak a secret to an attacker) invalidate that account’s tokens. dump and restore could allow you to bypass those checks if you dump state, shut pizauth down, change the configuration, and restore state. pizauth thus stores extra data about the secure aspects of your configuration in a dump: when restore is called, any changes between the dump’s view of an account’s configuration and the actual configuration cause tokens to be invalidated. This should stop you accidentally shooting yourself in the foot!

I hope those users who need state to be persisted find the new dump and restore functionality useful and usable — testing and comments are much appreciated! If they turn out to work well – and there are certainly more use cases for them than I’ve talked about in this post – then perhaps that 1.0.0 release is not too far away!

Newer 2023-04-03 10:00 Older
If you’d like updates on new blog posts: follow me on Mastodon or Twitter; or subscribe to the RSS feed; or subscribe to email updates:

Footnotes

[1]

For example, the number of side channels that people have uncovered in hardware in recent years is mind-boggling.

For example, the number of side channels that people have uncovered in hardware in recent years is mind-boggling.

[2]

Perhaps surprisingly, Age didn’t support passphrases for a large part of its early history. If nothing else, this would have made me nervous of accidentally copying the private key to an untrusted host. Passphrases aren’t a guarantee, but they make it much harder to recover the private key.

Perhaps surprisingly, Age didn’t support passphrases for a large part of its early history. If nothing else, this would have made me nervous of accidentally copying the private key to an untrusted host. Passphrases aren’t a guarantee, but they make it much harder to recover the private key.

[3]

token_event_cmd can discriminate based on the account name and token event kind but only users with very particular needs will ever need to do so.

token_event_cmd can discriminate based on the account name and token event kind but only users with very particular needs will ever need to do so.

Comments



(optional)
(used only to verify your comment: it is not displayed)