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
Google-Authenticator
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: https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/admin@docker.testnetwork.dom%3Fsecret%3DK437RBGN76YZasdfasdfgH3OIGA%26issuer%3Ddocker.testnetwork.dom BIG QRCODE HERE Your new secret key is: K437RBGN76YZasfdaasdfH3OIGA Enter code from app (-1 to skip): 678846 Your emergency scratch codes are: 76555272 26678300 59113801 35762901 29151634 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/test-pair.pub The key fingerprint is: SHA256:Iz411zY0p3rrtcyWu2jkgxiiJ2kVlguzGGhAHM user@fedora-kde The key's randomart image is: +--[ED25519 256]--+ ... +----[SHA256]-----+
Next, add the public key to the server using the “ssh-copy-id” command.
fedora-kde :: ~ » ssh-copy-id -i /home/user/.ssh/test-pair.pub admin@192.168.152.242
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/test-pair.pub 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@192.168.152.242 admin@192.168.152.242: Permission denied (publickey,gssapi-keyex,gssapi-with-mic). # With the key fedora-kde :: ~ » ssh admin@192.168.152.242 -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 #%PAM-1.0 auth substack password-auth auth include postlogin account required pam_sepermit.so account required pam_nologin.so account include password-auth password include password-auth # pam_selinux.so close should be the first session rule session required pam_selinux.so close session required pam_loginuid.so # pam_selinux.so open should only be followed by sessions to be executed in the user context session required pam_selinux.so open env_params session required pam_namespace.so session optional pam_keyinit.so force revoke session optional pam_motd.so session include password-auth session include postlogin auth required pam_google_authenticator.so
If you moved your .google_authenticator file, you can add the new location with this line. Replace the one above.
auth required pam_google_authenticator.so 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@192.168.152.242 -i ~/.ssh/test-pair (admin@192.168.152.242) Verification code: (admin@192.168.152.242) 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 0.0.0.0/0.
rocky-linux :: ~ » sudo vim /etc/ssh/sshd_config ... Match Address 10.254.0.0/16 AuthenticationMethods publickey Match Address 0.0.0.0/0 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 Hostname 192.168.152.242 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.