Using two factor authentication for ssh with Ubuntu 22.04

Since many of the instructions I found on the internet seem to be flawed or at least outdated, here is how I managed to enable two factor authentication (2fa) with (time based) OTP in Ubuntu 22.04 using the Google Authenticator PAM plugin.

Note: When using ssh private/public key based authentication, no OTP prompt will be shown. Apparently it is not possible to combine keys with OTP with this method.

Warning: If you make these changes while using an ssh session, do not close that session as you might not be able to log in again if there was a mistake.

  1. Install the Google Authenticator PAM plugin:
    sudo apt install libpam-google-authenticator
  2. Run the google-authenticator command as the user you want to log in (not as root):
     google-authenticator -t

    The option -t tells the command to generate a time based OTP which is nearly always what you want. It will produce the following output and prompt you for some input:

    Warning: pasting the following URL into your browser
    exposes the OTP secret to Google:
      [url not replicated here for security reasons]
    [There is a large QR code shown here]
    Your new secret key is: [a string of numbers and upper case letters]
    Enter code from app (-1 to skip): [see below for what to enter here]
    Code confirmed
    Your emergency scratch codes are:
      [first code]
      [seconde code]
      [third code]
      [fourth code]
      [fifth code]
    Do you want me to update your
    "/home/test/.google_authenticator" file? (y/n) y
    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
    By default, a new token is generated every 30 seconds
    by the mobile app. In order to compensate for possible
    time-skew between the client and the server, we allow
    an extra token before and after the current time.
    This allows for a time skew of up to 30 seconds between
    authentication server and client. If you experience
    problems with poor time synchronization, you can
    increase the window from its default size of 3 permitted
    codes (one previous code, the current code, the next code)
    to 17 permitted codes (the 8 previous codes, the current
    code, and the 8 next codes). This will permit for a time
    skew of up to 4 minutes between client and server.
    Do you want to do so? (y/n) n
    If the computer that you are logging into isn't hardened
    against brute-force login attempts, you can enable
    rate-limiting for the authentication module.
    By default, this limits attackers to no more than 3
    login attempts every 30s.
    Do you want to enable rate-limiting? (y/n) y
    • The first thing the command does, is output an url with a warning not to actually use it in the browser. You should heed that warning as the url contains the secret that is only meant for the authenticator app.
    • Then it draws a giant QR code and below that the secret key as a string of numbers and upper case letters. Open the OTP app you want to use (e.g. FreeOTP or Google Authenticator) and create a new entry by scanning the QR code. Alternatively enter the secret key.
    • Generate a code in the OTP app and enter it where the program prompts you. You can skip this step by entering -1 or by specifying the -C (upper case c) option to the command. I recommend not skipping it because it will tell you whether everything worked so far.
    • The five emergency scratch codes can be used to log in when for whatever reason the code generated by the OTP app fail. I think each of them can only be used once. You might want to write those down and keep them in a safe place.
    • Answer the question whether to update your .google_authenticator file with Y. The file created / updated will contain the secret key, the settings you will specify in the following questions and also the mergency scratch codes. Make sure that this file is only readable by the user. The command creates it that way, don’t change that.
    • There will be several more questions that require Yes or No as an answer. Here are my recommendations for the answers. If you think you know what you are doing, feel free to answer differently.
      • Answer the question regarding to disallow multiple uses of the same token with Y.
      • Answer the question regarding the window of permitted codes with N. Restricting the number of valid codes at any time to 3.
      • Answer the question regarding rate-limiting with Y.

    The google-authenticator command accepts additional options that may be interesting to use. Call it with –help to show them or look into its man page. E.g. all the questions it asks above can be answered using options. You can also specify the label and issuer shown in the OTP app.
    Note: This step must be repeated for every user to create an OTP secret for login.

  3. Add the pam module to the file /etc/pam.d/sshd:
    # Added to allow OTP as a second factor for ssh login
    auth required

    This line usually does not exist.
    Note: You can add nullok to the end of that line if you want to make the second factor optional for those users who did not create a secret key yet. But if you take security seriously, you should allow this only for a very limited time if at all.

  4. Add a new file 00-google-authenticator.conf to /etc/ssh/sshd_config.d:
    # special settings for using the Google Authenticator
    # IMPORTANT: For this to work
    # * The PAM module must have been installed
    # * and added to /etc/pam.d/sshd
    # These options must excplicitly be set, even though they might be the default.
    # sshd uses the first entry it finds and ignores any subsequent entries.
    # So if any other config file in /etc/ssh/sshd_config.d sets this later on
    # this entry will still be used.
    # KbdInteractiveAuthentication
    #   Specifies whether to allow keyboard-interactive authentication.
    #   The default is yes.
    #   The argument to this keyword must be yes or no.
    #   ChallengeResponseAuthentication is a deprecated alias for this.
    KbdInteractiveAuthentication yes
    # PasswordAuthentication
    #   Specifies whether password authentication is allowed.  The default is yes.
    PasswordAuthentication yes

    The first two characters of the file name are the number zero not the letter o. This prefix is used to make this file the first one that gets read.

  5. Restart the ssh daemon:
    sudo systemctl restart sshd.service

    Warning: If you have done the above using an ssh connection, do not close this session as you might not be able to log in again if there was a mistake. (Yes, I said that already, but people – some specific person (Yes that’s me) – forget anyway.)

  6. Try to log into the server using ssh. You should get a prompt similar to this:

    login as: test
    Keyboard-interactive authentication prompts from server:
    | Password:
    | Verification code:
    End of keyboard-interactive prompts from server

    The first prompt asks for your logon password. The second one for the code to generate with the OTP app. If everthing worked correctly, entering them should log you in.