Setup SSH with MFA

  • Post author:
  • Reading time:15 mins read
  • Post category:Linux
  • Post comments:0 Comments

Hello there,

Today, I want to write down the steps on how to set up MFA for SSH. I will also configure an exclusion for the internal network, just to showcase how that works. I created an Ansible role for this years ago, but I actually don’t remember how to configure it manually anymore. So I want to write that down.

I will use Rocky Linux 8.8 for this, but every major distribution should work fine.

Side note, I changed the gray text in the code sections. I did like the color scheme, but it’s not really readable if you are not using the dark mode.

Let’s begin.

Initial Setup


We will start with the pre requirements. Install the epel-release repository and “google-authenticator” after that. We will also need the qrencode packages.

# Install epel-release repository
rocky-linux :: ~ » sudo dnf install epel-release
# Install the google-authenticator PAM Module and qrencode
rocky-linux :: ~ » sudo dnf install google-authenticator qrencode qrencode-libs

Once that’s done, we can execute google-authenticator to generate our OTP secret.

Scan it with your favorite OTP software, I use “Aegis” on my phone, and type in the code. Answer the “update your .google_authenticator file” with yes, for the rest you can choose what is most appropriate for your environment.

rocky-linux :: ~ » google-authenticator
Do you want authentication tokens to be time-based (y/n) y
Warning: pasting the following URL into your browser exposes the OTP secret to Google:|0&cht=qr&chl=otpauth://totp/admin@docker.testnetwork.dom%3Fsecret%3DK437RBGN76YZasdfasdfgH3OIGA%26issuer%3Ddocker.testnetwork.dom


Your new secret key is: K437RBGN76YZasfdaasdfH3OIGA
Enter code from app (-1 to skip): 678846
Your emergency scratch codes are:

Do you want me to update your "/home/admin/.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) n

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) y

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

Setting up the ssh key pair

For the MFA, I also want to use an authentication key. So let’s create one.

On your client execute the “ssh-keygen” command, to generate a new key pair.

fedora-kde :: ~ » ssh-keygen -t ed25519 -f ~/.ssh/test-pair
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/user/.ssh/test-pair
Your public key has been saved in /home/user/.ssh/
The key fingerprint is:
SHA256:Iz411zY0p3rrtcyWu2jkgxiiJ2kVlguzGGhAHM user@fedora-kde
The key's randomart image is:
+--[ED25519 256]--+

Next, add the public key to the server using the “ssh-copy-id” command.

fedora-kde :: ~ » ssh-copy-id -i /home/user/.ssh/ admin@

After entering your credentials, the public key should be on the server. We can verify this. First check the public key on your client.

fedora-kde :: ~ » cat /home/user/.ssh/
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO5Nonydyp605WceZnpggzxg3ba8AA3yjhEwcjBGjhS9 user@fedora-kde

Alright. Now compare it to the server.

rocky-linux :: ~ » cat /home/admin/.ssh/authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO5Nonydyp605WceZnpggzxg3ba8AA3yjhEwcjBGjhS9 user@fedora-kde

Great. Let’s test this first. Disable the “PasswordAuthentication” in the sshd_config file and restart the sshd service.

rocky-linux :: ~ » sudo vim /etc/ssh/sshd_config
PasswordAuthentication no

rocky-linux :: ~ » sudo systemctl restart sshd

Now. I recommend doing the tests with another terminal and keeping the current session active, in case something went wrong, and you locked yourself out. So, open a new terminal on your client and test the connection.

# Without the key
fedora-kde :: ~ » ssh admin@
admin@ Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

# With the key
fedora-kde :: ~ » ssh admin@ -i /home/user/.ssh/test-pair
Enter passphrase for key '/home/user/.ssh/test-pair': 
[admin@rocky-linux ~]$ 

Fantastic. Next we can continue with the PAM configuration.

Setting up PAM

Let’s continue with the ssh pam file. Open the /etc/pam.d/sshd file and append the following line in red.

rocky-linux :: ~ » sudo vim /etc/pam.d/sshd
auth       substack     password-auth
auth       include      postlogin
account    required
account    required
account    include      password-auth
password   include      password-auth
# close should be the first session rule
session    required close
session    required
# open should only be followed by sessions to be executed in the user context
session    required open env_params
session    required
session    optional force revoke
session    optional
session    include      password-auth
session    include      postlogin
auth required

If you moved your .google_authenticator file, you can add the new location with this line. Replace the one above.

auth required secret=${HOME}/.ssh/.google_authenticator

Setting up SSH

Next, configure the sshd_config file and edit the following lines.

rocky-linux :: ~ » sudo vim /etc/ssh/sshd_config
ChallengeResponseAuthentication yes
UsePAM yes
Match all
     AuthenticationMethods publickey,keyboard-interactive

Restart the sshd service and test the login. Again, use a different terminal and keep the current session open.

rocky-linux :: ~ » sudo systemctl restart sshd
fedora-kde :: ~ » ssh admin@ -i ~/.ssh/test-pair
(admin@ Verification code: 
(admin@ Password: 
[admin@rocky-linux ~]$ 

The login required the key + user password + the OTP code. This should be good enough. Now, before I wrap things up, I want to set it up to skip the OTP code in the local network or a network of my choosing.

For this, edit the sshd_config file again, and change the “Match all” line to “Match Address <ip-address-or-network>”. The first line should be your network in which you want to skip the check, and at the end the catch all rule with

rocky-linux :: ~ » sudo vim /etc/ssh/sshd_config
Match Address
        AuthenticationMethods publickey
Match Address
        AuthenticationMethods publickey,keyboard-interactive

Restart your sshd service again and you should be able to log in with only your key.

Keep in mind, that this, somewhat, defeats the purpose of setting up MFA. A use case would be for service accounts, like a borgbackup, Ansible or something similar, where you can’t type in the OTP code every time. For this you can use the “Match User” condition. It would look like this.

rocky-linux :: ~ » sudo vim /etc/ssh/sshd_config
Match User borgbackup
       AuthenticationMethods publickey

SSH config file (Optional)

One more little extra thing for your ssh connection. If you don’t want to type out the whole ssh command every time, you can create a ssh config file under .ssh with all the information.

fedora-kde :: ~ » vim .ssh/config
Host rocky-linux
   IdentityFile ~/.ssh/test-pair
   user admin

Now you can just type the “host” rather than the whole command with the key in the CLI.

fedora-kde :: ~ » ssh rocky-linux

That’s it. It took me awhile till I figured out how to get this working again, but now I have it in writing for the next time. Hope this helps you too.

Till next time.

Leave a Reply