Local MFA token

Recently my phone went out of order for a day. I immediately switched over to my backup phone and was back to normal after one hour.

This was a great opportunity to check if I still had all T’s crossed and I’s dotted and full control over my data in case of an unexpected event - like missing the phone.

Surprise: I wasn’t fully prepared.

While most applications and their data were recovered, easily restored and no data was lost, I realized that the Multi-Factor-token generator that I use on my phone had no backup. I used it everyday to get access to customer sites and resources.

Using my phone as the token generator had put me in a situation where I was dependent on that particular phone. Some websites offer one-time codes during the setup of the MFA and those I still had, but the token generator itself was a single point of failure.

Gaining access

Out of pure luck I was able to return to a stable phone situation within a day. That made looking at that problem easier.

The One-time-password (OTP) generator I use on the phone allows me to export and import the accounts. The exported data also includes the secret used to setup the account/token. Interesting.

I would rather not have that come astray in a random backup being taken in the background. I wanted a fallback application on my desktop in order to be able to generate the tokens there as well in case I needed to.

Security

Almost all secrets I have are stored in pass and that keeps them GPG encrypted. This is the natural place for the token secrets as well in my case. The solution needed to be able to fetch the secrets dynamically from the store and use them to generate the one-time passwords.

Sounds do-able.

A solution forms.

The tool oathtool is precisely designed to generate one-time passwords from secrets. Neat.

The command for generating a token would look like this:

$ oathtool --totp --base32 -d 6 $(echo '$secret' | base32)
055131
Click to expand...
  • --totp: The default for this parameter is SHA1, which is identical to the information I got from the exported file from the token generator.
  • --base32|-b: The tokens are usually base43 encoded, so we do this as well.
  • --digits|-d: The default number of digits according to my export was six. This parameter defines the same number of digits in the one-time password.

Now we need to fetch the secret from the password store and place it inside the command.

$ oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1)
636296

That seems to work. The returned token has changed, since some minutes had passed since I ran it the last time.

Quick and dirty and secure integration

Now, how to integrate this rather messy command into the daily workflow?

For starters I add a bash alias:

alias otp.customer_a="oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1)"

As mentioned in this article this is rather simple in my setup, but an equivalent entry in .bashrc should do the trick as well.

However: This does not work.

When running the alias the one-time password never changes. It stays the same, no matter how long I wait.

I managed to stumble on a quirky limitation of bash alias. Aliases evaluate their command when they are being defined, not when they are being executed. That means the alias otp.customer_a was not calling the password store to fetch the secret, but rather had done this already once when initialised. Just check the alias definition:

# Set alias
$ alias otp.customer_a="oathtool --totp -b -d 6 $(pass show otp/customer_a | head -n 1)"
# Show alias
$ alias otp.customer_a
alias otp.customer_a='oathtool --totp -b -d 6 secret_string'

Let’s re-arrange the command a bit to solve this:

$ alias otp.customer_a="pass show otp/customer_a | head -n 1 | tr -d '\n' | xargs -0 oathtool --totp -b -d 6"
$ alias otp.customer_a
alias otp.customer_a='pass show otp/customer_a | head -n 1 | tr -d '\''\n'\'' | xargs -0 oathtool --totp -b -d 6'
Click expand...
  • Now the alias executes the pass command every time.
  • head -n 1 limits the return from pass to the first line. That line contains usually the password (in this case the secret) string.
  • td -d '\n' removes any trailing newlines. The additional '-characters just escape the ones in the command.
  • xargs -0 hands the result from pass over to oathtool.

The result is local one-time password generator and backup in case the phone is not available. Still the secrets are protected. Probably better than on the phone.

otp.customer_a
112233

A quick verification of the codes with the phone based generator showed the same results and I was able to get access using this one-time passwords as well.

Lessons learned

  1. I should keep up to date with the data and configuration on my phone and think more often about security in that context. Though the phone itself is protected against malicious access, there are a couple of more steps that can be taken to increase the security further.
  2. I need to store the secret for the MFA setup safe in order to be able to restore access to customer resources more quickly.
  3. Other procedures for setting up the backup-phone worked as planned and no data would be lost in case of a loss.
  4. Reading the man page of bash back in the day did more good than harm. Though it’s quite long, I still benefit from it when I can recall those limitation on alias.

Update 2022-11-25

A colleague hinted that pass already has a otp-plugin available.

After a quick look it looks like I have re-invented the wheel. That happens in the middle of a crisis.

The installation is (under Fedora) fairly simple:

sudo dnf install -y pass-otp

All other steps in the post above can be compressed further:

Put the OTP key in URI format into pass.

$ pass otp insert otp/customer_a
Enter otpauth:// URI for customer_a:
Retype otpauth:// URI for customer_a:

All this does is putting the string into the pass GPG file at the first line. Adding additional information into the other lines below will work as usual.

Fetch the OTP token when needed:

$ pass otp otp/customer_a
123456

Configure the alias:

alias otp.customer_a="pass otp otp/customer_a"

This solution is cleaner, easier to implement and to maintain as well. Thanks @kid for pointing to that plugin. Guess I have to go and re-configure this now really quick.

Update 2023-05-10

Using this over several months made me add a small extension to the alias.

alias otp.customer_a="pass otp otp/customer_a | xclip && echo 'OTP copied to clipboard.'"

Daniel Buøy-Vehn

Senior Systems Consultant at Redpill Linpro

Daniel works with automation in the realm of Ansible, AWX, Tower, Terraform and Puppet. He rolls out mainly to our customer in Norway to assist them with the integration and automation projects.

Thoughts on the CrowdStrike Outage

Unless you’ve been living under a rock, you probably know that last Friday a global crash of computer systems caused by ‘CrowdStrike’ led to widespread chaos and mayhem: flights were cancelled, shops closed their doors, even some hospitals and pharmacies were affected. When things like this happen, I first have a smug feeling “this would never happen at our place”, then I start thinking. Could it?

Broken Software Updates

Our department do take responsibility for keeping quite a lot ... [continue reading]

Alarms made right

Published on June 27, 2024

Just-Make-toolbox

Published on March 22, 2024