Automating SSH Login Involving TOTP Codes

In my day job I work with a lot of Linux servers, all only accessible via a bastion host. To make things slightly more complicated it is a password-based login with a TOTP1 two-factor step. I use SSH ControlMaster but still need to log in frequently.

The 2FA setup makes automation difficult, or at least so I assumed, but the other day I finally managed to solve it.

I am very happy and paid-up user of BitWarden (my vault contains over 350 passwords), and my usage in this scenario would typically involve switching over to my browser, opening the extension then searching for and copying the password, back to my terminal to paste it in, back to the browser to search for and copy the TOTP code, and finally back to the terminal again to finalise login. It beats remembering and typing, but it’s still frustrating.

BitWarden has a number of clients, including for the command-line, but because the vast majority of my usage is browser-based I’d never bothered downloading it. It did seem like the perfect candidate to automate my password-entry papercuts though.

Bitwarden Usage

Installation of the CLI is straightfoward, either via npm or several package-management options.

Usage is also fairly simple:

  • bw unlock will unlock your vault (and bw status reports several statistics included locked status).
  • Unlocking outputs a line to paste into your shell that sets an environment variable for the current session.2
  • bw get password <item-id> will print the plain-text password for an item. You can of course search for items to find this id.

Output is in JSON, if you need to script anything in conjunction with jq for example.

As a bonus, you can enable shell completion via the tool itself:

eval "$(bw completion --shell zsh); compdef _bw bw;"

First attempt

When I was noodling this idea around my first thought was some variant of expect.

However: expect is designed around automating the entire session, whereas all I want is to be dropped into a shell with less typing. So, that wasn’t going to work.

Second, successful, attempt

SSH goes to some lengths to avoid this type of automation, but it is feasible. There is a tool that allows several methods of password entry (file, CLI, environment variable) and passes it to SSH:

Disclaimer: Possibly like you, I saw “sourceforge” and also assumed it must be abandoned. However it does appear that the maintainer, while busy, is interested in considering modern needs for the tool (just not adding patches that solve one specific case but not the general requirements).

As of now it doesn’t support TOTP codes though, but there are numerous forks with varying approaches; I settled on

This variant supports TOTP codes by specifying an executable file which generates a code. Ours just needs to be a script containing bw get totp <id>.

I didn’t want to have a file containing my password, even temporarily, so my first instinct was to use bw to pipe the password in; eg

bw get password $passwordid | sshpass....

This doesn’t work because ssh then realises that stdin is redirected and doesn’t open a pseudo-tty.

So, my ultimate solution was to use an environment variable, just for that invocation (-e to indicate it should look for a variable, and you probably also want to configure the password and TOTP prompts):

SSHPASS=$(bw get password $passwordid) sshpass -e -c generate-totp ssh



Right now this is what I do, and it’s only a control-r away (even easier to search for since I’ve started using fzf). I did look at turning it into a command for the purposes of this post, but there were a few issues:

  • an alias is problematic because of the amount of quoting, and not wanting the get-password sub-shell to be prematurely evaluated.
  • A shell function would be ideal, but… doesn’t give me an interactive prompt at the end.

I’m sure it’s solvable, but for a quick post I didn’t invest enough effort — sorry!

  1. Time-based One-Time Password ↩︎

  2. Unfortunately it doesn’t allow this to be automated, along the lines of ssh-agent, for eg “eval $(bw unlock)”. Update: yes it does! export BW_SESSION=$(bw unlock --raw) ↩︎


694 Words

2021-08-21 00:00 +0000

comments powered by Disqus