Securing your server with 2FA

I have a bunch of servers running within the company network. These server running multiple virtual machines such that these servers can be seen as a hypervisor. I connect to them using ssh and for additional security I use a second factor. 2FA or two-factor authentification can be used to improve the security by forcing the user not only provide the correct ssh key but also another secret shared by the machine and the user. This corrects the case that a ssh key of a users gets compromised. On the backend I use the PAM module Google Authenticator which generates tokens via the TOTP protocoll. This is also called OATH-TOTP which stands for Open Authentication - Time-Based One-Time Password. Initially the server and the user exchanges a shared salt which should be unknown to all others. Afterwards, the user can generate a 6 digit token based on that salt and the current time. The servers has the same informations and generates the same token for validation.


First we install an app which supports TOTP on another device (maybe your smartphone). I use FreeOTP which I installed from the F-Droid store. The interface is fairly simple and should be self-explaining. We use QR codes for the initial exchange but one can enter all informations by hand.

On the server-side we install the PAM module with:

 1apt install libpam-google-authenticator
 2# Run the setup
 4# Question time
 5Do you want authentication tokens to be time-based (y/n) y
 6# The initial qr code is generated, scan it with your new app and save the informations in a safe place
 8# Write the secret informations into your user directory or nothing works
 9Do you want me to update your "/<user>/.google_authenticator" file? (y/n) y
10# Prevent man-in-the-middle attacks
11Do you want to disallow multiple uses of the same authentication
12token? This restricts you to one login about every 30s, but it increases
13your chances to notice or even prevent man-in-the-middle attacks (y/n) y
14# We only use three codes (the last, the current and the next one)
15Do you want to do so? (y/n) n
16# Enable rate-limiting for authentication
17Do you want to enable rate-limiting (y/n) y


After the question time the PAM module is initialised and preconfigured but not active. Now, we enable 2FA for ssh by changing the file mg /etc/pam.d/sshd and adding following the line on the top.

1auth required nullok [echo_verification_code] [authtok_prompt=2F-Token: ]
2# Comment out the normal login mechanics
3#@include common-auth
  • auth required triggers that these line must be matched to grant access.
  • nullok means that logins without 2FA are accepted. This can be changed if all users have 2FA enabled.
  • [authtok_prompt=2F-Token: ] is the prompt used to question the user for 2FA.

Now we make ssh aware of our 2FA setup by changing the following lines in the configuration (mg /etc/ssh/sshd_config):

1# Enable challange response keys
2ChallengeResponseAuthentication yes
3# Enable pam aware login
4PAM yes
5AuthenticationMethods publickey,keyboard-interactive:pam

Reload your ssh daemon with systemctl reload sshd. Now you need to use 2FA for your next login. If you want that new users can setup 2FA on the first login you can use a script which generates the ~/.google_authenticator file.

1# Open the profile file
2mg /etc/profile
3# Add the following lines at the bottom of the file
4test "$(id -u)" -eq 0 || test -e ~/.google_authenticator || {
5  read -p "Do you want to setup 2FA now? [Y/n]" SETUP
6  test "$SETUP" = "n" || {
7    google-authenticator -t --rate-limit=3 --rate-time=30 --window-size=17 --force --disallow-reuse
8  }

For automatic creation of 2FA for new users you need the following line in the pam.d/sshd at the end of the file.

auth required

This piece of code and the nullok allow users to bypass the 2FA forever since you can login without the setup. So, encourage your users to use 2FA and change booth lines later to only allow 2FA. Otherwise, you can use authorized users or groups lists within PAM to get a more secure setup.

Be aware, that you should backup your 2FA secrets in case you lost your phone or it gets damaged!

Backup TOPT tokens

To backup my tokens I use emacs and followed this guide. It uses a big table to represent all the fields needed to generate the tokens and QR codes.

The table:

1#+TBLNAME: otp-tokens
2| Issuer | Label                  | Secret           | URI                                                                                |
4| Google | | fakesecret123456 | otpauth://totp/ |
5#+TBLFM: $4='(concat "otpauth://totp/" (if (string-blank-p $1) "" (concat $1 ":")) $2 "?" (url-build-query-string `(("secret" $3) ("issuer" $1))))

The explaination:

  • Issuer: Name of the service or machine
  • Label: The account identifier
  • Secret: The base32 encoded shared secret
  • URI: The configuration content of the QR code. This is created by the TBLFM

You can either fill the informations into the table from the .google_authenticator file and your own knowledge or use a script which fetches the informations from FreeOTP.

Afterwards, put your curser somewhere on the table and run the table function with C-c C-c. To generate a nice QR code use python and the tool qrencode:

1#+name: qrcodes
2#+begin_src python :var tokens=otp-tokens :results output
3import subprocess
4for issuer, label, secret, image, uri in tokens:
5    print("%s (%s)\n" % (label, issuer if len(issuer) > 0 else "n/a"))
6    code = subprocess.check_output(["qrencode", "-t", "UTF8", uri])
7    print(code.decode("utf-8"))
8    print("\n"*30)

Running this with C-c C-c generates a result block with the QR code in it. Now, you can simply scan the code again. If babel complains about python, make sure that python is within the org-babel-load-languages list.

The original author provided the following piece of python to fetch the secrets directly from the FreeOTP app. The secret and the additional informations shall be copied into the table. I didn’t tried it but would mention it. You need developer mode active on your phone and maybe the cmd path needs to be adjusted.

 1#+name: free_otp
 2#+begin_src python :results output
 3from xml.etree import ElementTree as ET
 4import base64, json, subprocess, sys
 6cmd = ["adb", "shell", 'su -c "cat /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml"']
 7ret =, stdout=subprocess.PIPE, check=True)
 8root = ET.fromstring(ret.stdout)
 9for child in root:
10  name = child.attrib['name']
11  if child.attrib['name'] != 'tokenOrder':
12    data = json.loads(child.text)['secret']
13    data = ''.join('%02x' % (x % 256) for x in data)
14    secret = base64.b32encode(bytes.fromhex(data)).decode('utf-8').rstrip('=').lower()
15    print(name, ' ' * (40 - len(name)), secret)

Thanks for your attention and leave me a comment.