Securing your server with 2FA

2021-10-20

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.

Installation

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:

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

Configuration

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.

1
2
3
auth required pam_google_authenticator.so nullok [echo_verification_code] [authtok_prompt=2F-Token: ]
# Comment out the normal login mechanics
#@include common-auth
  • auth required pam_google_authenticator.so 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
2
3
4
5
# Enable challange response keys
ChallengeResponseAuthentication yes
# Enable pam aware login
PAM yes
AuthenticationMethods 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
2
3
4
5
6
7
8
9
# Open the profile file
mg /etc/profile
# Add the following lines at the bottom of the file
test "$(id -u)" -eq 0 || test -e ~/.google_authenticator || {
  read -p "Do you want to setup 2FA now? [Y/n]" SETUP
  test "$SETUP" = "n" || {
    google-authenticator -t --rate-limit=3 --rate-time=30 --window-size=17 --force --disallow-reuse
  }
}

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 pam_permit.so

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
2
3
4
5
#+TBLNAME: otp-tokens
| Issuer | Label                  | Secret           | URI                                                                                |
|--------+------------------------+------------------+------------------------------------------------------------------------------------|
| Google | fake.address@gmail.com | fakesecret123456 | otpauth://totp/Google:fake.address@gmail.com?secret=fakesecret123456&issuer=Google |
#+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
2
3
4
5
6
7
8
9
#+name: qrcodes
#+begin_src python :var tokens=otp-tokens :results output
import subprocess
for issuer, label, secret, image, uri in tokens:
    print("%s (%s)\n" % (label, issuer if len(issuer) > 0 else "n/a"))
    code = subprocess.check_output(["qrencode", "-t", "UTF8", uri])
    print(code.decode("utf-8"))
    print("\n"*30)
#+end_src

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#+name: free_otp
#+begin_src python :results output
from xml.etree import ElementTree as ET
import base64, json, subprocess, sys

cmd = ["adb", "shell", 'su -c "cat /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml"']
ret = subprocess.run(cmd, stdout=subprocess.PIPE, check=True)
root = ET.fromstring(ret.stdout)
for child in root:
  name = child.attrib['name']
  if child.attrib['name'] != 'tokenOrder':
    data = json.loads(child.text)['secret']
    data = ''.join('%02x' % (x % 256) for x in data)
    secret = base64.b32encode(bytes.fromhex(data)).decode('utf-8').rstrip('=').lower()
    print(name, ' ' * (40 - len(name)), secret)
#+end_src

Thanks for your attention and leave me a comment.