#TOTP for SSH

I mostly use public key authentication with SSH: between machines and software stacks I trust, this is sufficient.

Sometimes, however, I find a need to SSH from a machine I don't fully trust to a high-value target host. For example: sometimes I find a need to SSH from a work machine (which any number of people I don't know have access to) to one of my personal machines. While it would be ideal never to have to do this, sometimes it's simply the easiest or most timely way to get something done.

Another example: when I'm using my live USB install on someone else's hardware. In such a scenario, I can't guarantee the hardware or BIOS haven't been hacked to scrape everything I'm doing into a convenient package that could be used later to regain access to any machine I SSH to.

Finally, and more mundanely: I prudently forward my agent only to other machines and software stacks I trust, so if I need to SSH back for some reason, I want some way of doing this without exposing my agent's keys to a low-trust machine.

How to handle this? The best way is to limit a login to a single session that I explicitly initiate and can presume some broad degree of control over. This obviously isn't airtight because an adversary could take control of the machine immediately after I initiate the session, but my threat model is not a live attacker but someone scraping authentication credentials for later, surreptitious use.

The mechanism I settled on was to require TOTP for logins using a set of special, lower-trust SSH keys, and to configure the TOTP validator to accept a given code exactly once within the window of validity. This is a recipe for setting that up, based on this (mostly fabulous) guide provided by Digital Ocean.

#Create a TOTP seed on the target server

This installation guide presumes your server is running Debian or Ubuntu. Other repos presumably have equivalent functionality.

On the server, install the TOTP PAM package, called libpam-google-authenticator (presumably named such because Google was the creator of the first widespread TOTP app for phones, which they wanted for their user authentication infrastructure). Then, create a new seed as the target user using the google-authenticator tool provided by that library. I use the following command line:

google-authenticator -f -t -d -u -W

This generates a config for a time-based authenticator (-t) with a minimal time skew window (-W), without rate limiting (-u), disallowing code reuse (-d), and forces writing of the new config without confirmation (-f). Feel free to play with these settings if you want different behavior. Along with the QR code to configure your TOTP app, this command will output some emergency codes you can add to a password manager, if desired. At this point you'll need to confirm your authenticator has been configured correctly by entering a code; you can skip this step with the -C argument.

#Configure PAM for SSH to require a TOTP code for keyboard-interactive auth

Edit /etc/pam.d/sshd. Comment out the line near the top that says @include common-auth by prefixing it with a hash (#): this will stop SSH from asking for a password during keyboard-interactive authentication. (You already weren't allowing password logins, right? Right?) Then, add the following line to the very end of the file:

auth required pam_google_authenticator.so

This will require a valid TOTP config and for the user to enter a code valid for that config whenever keyboard-interactive auth is used: anyone without a configured TOTP config will still be prompted for codes to minimize information leakage to an attacker, but the PAM module will always fail on such attempts. (You can observe this in auth.log if you attempt to login as a user without a TOTP config.)

#Configure OpenSSH to run on another port with keyboard-interactive auth required

Now you'll modify your sshd config. (If you're doing this remotely, make sure to keep a live session open throughout so you can recover without needing console access. You can restart sshd and test while keeping an existing session open.) I recommend making these config changes in a file in /etc/ssh/sshd_config.d rather than modifying the vanilla config in /etc/ssh/sshd_config: I use 03totp.conf for this, but I think anything ending with .conf in that directory will work.

By default, disable all interactive logins:

PasswordAuthentication no
KbdInteractiveAuthentication no

But then listen on port 9022 (or some other port of your choosing) and require both public key authentication and keyboard-interactive authentication on logins to that port, adding an additional authorized keys file (here, ~/.ssh/authorized_keys_totp) specifically for less-trusted keys:

Port 9022

Match LocalPort 9022
    KbdInteractiveAuthentication yes
    AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys_totp
    AuthenticationMethods publickey,keyboard-interactive:pam

(Note that this port also accepts the usual high-trust keys in authorized_keys, mostly for testing TOTP authentication.)

Restart ssh (e.g., systemctl restart ssh.service) and modify your server's firewall rules to allow port 9022 access (if necessary).

#The Moment of Truth

Test the login to port 9022 from another terminal:

ssh -S none -o KbdInteractiveAuthentication=yes target.server.example.com -p 9022

You should be prompted for your TOTP code. (Note the use of -S none to disable any use of a control master session, and -o KbdInteractiveAuthentication=yes to enable interactive authentication in case you have it disabled by default in your client config.) Assuming this worked, now test port 22 and confirm you are not prompted for a TOTP code:

ssh -S none -o KbdInteractiveAuthentication=yes target.server.example.com

Voila! You are done. You may now create new less-trusted keys and add them to ~/.ssh/authorized_keys_totp so they won't be accepted on port 22 and will therefore always require a valid TOTP code.

#Additional thoughts