Despite my reservations about two factor auth, I decided to try implementing it. Don’t knock it til you’ve done it, right? I’ve previously played with Duo Security’s login_duo, and they have a nicely polished mobile app, but the command line tool doesn’t quite feel integrated with the rest of the system (it’s user opt-in, not admin mandated). Plus, it’s more fun to build your own. For this experiment, I picked Pushover as factor number two, which also comes with a nice app which can be used for other things as well. Now we just need some code to talk to Pushover.
Pushover is a web hook to smartphone push notification gateway. POST to a web server and it sends a message to your phone. Sounds perfect. We just need some plumbing to get from login to Pushover. After about five seconds of investigation, I realized that sending a challenge token to Pushover involves making an HTTPS request. After a further three seconds of thought, I realized I didn’t want to write the C code to make that happen. Perfect time to use Go. Just one problem. Go reserves a buttload of memory (address space) when it starts, even if it won’t ever use it, and bumps into the default resource limits. Back to C.
Solving this requires using two programs, login_pushover and pushover_authd. Fortunately, this works quite well and is an improvement in many ways. Talking to Pushover requires some additional state, like user key and time elapsed since the last challenge, that’s best kept in a long running daemon to avoid filesystem races. login_pushover is pretty specific to OpenBSD systems, but pushover_authd is theoretically generally useful.
login_pushover is the program login and sshd and... (ftpd?) will use to verify your password. It’s written in C and speaks a very simple HTTP/1.0 dialect to pushover_authd. Using it requires editing /etc/login.conf to create a new tfa class that uses the pushover auth method. The example inherits the rest of its properties from default, but you’ll likely need to change to staff or whatever is appropriate.
Only root can change a user’s login class, so the next step is to run vipw and change your login class. It’s the field after gid. Also, if you’ve previously compiled login.conf, you’re a maniac and you’ll need to recompile it. With that out of the way, here’s what it looks like in action:
The prompt says “pushover+password” because we’re going to enter both, separated by a + sign. login_pushover knows the challenge will never contain a + which lets it find the password. Speaking of the challenge, here it is:
We enter the above code plus our password and login_pushover tells sshd to let us in.
How does login_pushover know to let us in? Because pushover_authd said so (and our password checked out). This is the part written in Go. It’s one part web server and one part web client. It listens for requests to issue new challenges. When it receives a request, it checks the cache first. If we’ve sent a challenge recently, we don’t need to do anything. If we miss the cache, we generate a new random challenge and send it out via Pushover. We never return the challenge to the client. Instead, login_pushover will contact us a second time to request that we verify a challenge is correct. Challenges expire after one to two minutes, or when successfully verified. A challenge is ten letters or numbers long, minus a few combos that are easily confused (e.g., O and 0).
pushover_authd requires some configuration consisting of the Pushover appkey and end user keys (you need to provide your own, I’m not giving you mine). It’s just a simple text file, which you pass as the first argument. pushover_authd config.txt
By default, pushover_authd listens for connections on a unix socket, but it can use the internet if you’d like to use it for something else, like a web app. (login_pushover can also speak internet, if you have a cluster of machines and would like to share one pushover_authd.) Add a listen :8080 line to the config to listen on port 8080. The two supported requests are /challenge?user=ABC and /verify?user=ABC&challenge=XYZ. Both requests reply with a simple body of “ok”. Failed verifications will return “fail” with a status code of 200. The HTTP 403 status would imply you lack permission to issue the query, which isn’t true. If you can talk to pushover_authd, you’re authorized to talk to it.
If the appkey is missing or the requested user doesn’t exist, a challenge is still generated, but logged to stdout for debugging. pushover_authd rereads the config file for every push, allowing config changes on the fly. It could talk to some fancy LDAP backend, except it can’t.
login_pushover implements a strict two factor auth system. You need the challenge and your password. It doesn’t permit recovering lost passwords, nor is there any way to bypass it if you lose your phone (short of the obvious: ssh keys). What use is two factor auth if it’s really “two except when it’s one”? If Pushover decides to conspire against you, they cannot login to your system by stealing the challenges, but they can prevent you from logging in by not delivering them. Keep that in mind. The login.conf example above requires pushover, but you could also configure skey as a backup.
login_pushover itself is setgid shadow because it needs to read the shadowed password database.
pushover_authd isn’t stupidly insecure, but it doesn’t authorize its clients. Use physical separation for that. By default, the /var/run/pushover_authd directory is owned by root:_shadow with 0550 permissions, which prevents local users from poking it uncontrollably, though there’s little point. If somebody wants to harass you late at night, they just try to ssh in as your username. That’s what quiet hours are for.
The Makefile takes care of most of this.
(Duo Security does win a gold star here, because they enforce the second factor after successful primary login. Duo also allows enforcing two factor auth with ssh keys. That would kill me.)
login_pushover on github. login_pushover builds on OpenBSD. pushover_authd should build where there is Go.