#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
-
Do not use control master sessions from less trusted clients. This advice actually isn't specific to TOTP logins, but is especially important for the threat model addressed here. Specify
-S none
, or better yet disable control master by config on less-trusted clients, so no one else can piggyback on your session. (While it would be nice to automate this via a remote port match in the config, OpenSSH doesn't currently have this capability.) TOTP aside, on less-trusted clients you don't want to open a window in which an adversary with simultaneous access to the machine can silently make use of a live session without your knowledge. -
Using a separate port is the only way I've found to distinguish TOTP-required login requests for an arbitrary user from regular public key authentication requests for the same user. If there's a better way (e.g., using some
authorized_keys
setting), please let me know, as that would simplify things significantly! -
Is there a server-side way to disable connection multiplexing? I'd rather not rely on client configuration if possible.
MaxSessions 1
doesn't solve this problem because the adversary can still use the session after you terminate the last/only connection until the session times out.