Saltar al contenido principal

% 16k.es

Enabling 2FA on RHEL 8 using Google Authenticator

Table of Contents

Enabling 2FA on RHEL 8 using Google Authenticator is easy… not. Especially if SELinux is enforced.

First things first. Let’s install google-authenticator. You need to enable EPEL repository and install google-authenticator and, most likely, also qrencode in order to be able to create the usual QR. Of course, install the app in your phone too. By the way, it does not have to be the Google one, there are multiple options in the marketplaces.

I would suggest to try to get a grip on how PAM works. An introduction to Pluggable Authentication Modules (PAM) in Linux can be a good start.

## Configuring 2FA for ssh

When enabling 2FA on RHEL (and RHEL variants like CentOS, Rocky Linux, etc.) most people thinks of ssh. Ok, it makes sense, RHEL derivatives are commonly users as servers.

So let’s go:

yum -y install google-authenticator qrencode
user@host$ 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/...secret...

(Big QR here if qrencode was installed)

Your new secret key is: KTMWDPAUYTMQPNWHFUIAXBXXMA
Enter code from app (-1 to skip): 358739
Code incorrect (correct code 820697). Try again.
Enter code from app (-1 to skip): 820697
Code confirmed
Your emergency scratch codes are:
  23949500
  33803586
  12458013
  68333993
  78368786

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

Cool, now we have a .google_authenticator file with our secret, configuration and some one-time emergency tokens we could use in emergency cases.

user@host$ cat $HOME/.google_authenticator 
KTMWDPAUYTMQPNWHFUIAXBXXMA
" RATE_LIMIT 3 30
" WINDOW_SIZE 17
" TOTP_AUTH
23949500
33803586
12458013
68333993
78368786

(Note: no, I don’t care about sharing this secret publicly, it does not exist anymore)

So far so good. Now we have to configure PAM (Pluggable Authentication Modules) and ssh configuration.

For PAM we just have to add this line:

auth required pam_google_authenticator.so

at the end of the PAM SSH configuration file: /etc/pam.d/sshd.

And make sure challenge ChallengeResponseAuthentication yes is present so reponse authentication is enabled.

That’s all, let’s check everything works:

user@host$ ssh user@localhost
Password: 
Verification code: 
Password: 
Verification code: 
Password: 
Verification code: 
Received disconnect from ::1 port 22:2: Too many authentication failures
Disconnected from ::1 port 22
user@host$ 

What’s happening here? It should work!

Ok, let’s do the obvious first troubleshooting step:

user@host$ sudo setenforce 0
user@host$ ssh user@localhost
Password: 
Verification code: 

Last failed login: Fri Jan 14 21:28:25 CET 2022 from ::1 on ssh:notty
There were 3 failed login attempts since the last successful login.
Last login: Fri Jan 14 21:27:30 2022 from ::1
user@host$ 

We’re in! F*ck SELinux! Well, no. The problem is SELinux does not allow ssh to read the secret file:

Jan 14 21:34:44 host sshd(pam_google_authenticator)[19454]: Failed to read “/home/user/.google_authenticator” for “user”. In some cases, ssh/PAM may need to write a file with a random name before updating the secret file. Unfortunately there is not an easy solution for this except to move the secret file to a directory with the right SELinux context: $HOME/.ssh. But we will have to do a slight modification to the PAM configuration file:

auth required pam_google_authenticator.so secret=/home/${USER}/.ssh/.google_authenticator

Move the secret file to .ssh and make sure the context is right:

user@host$ ls -laZ .ssh/
total 40
drwx------.  2 user user unconfined_u:object_r:ssh_home_t:s0       180 Jan 14 21:47 .
drwx------. 20 user user unconfined_u:object_r:user_home_dir_t:s0 4096 Jan 14 21:45 ..
-rw-r-----.  1 user user unconfined_u:object_r:ssh_home_t:s0       138 Jan  8 19:16 config
-r--------.  1 user user system_u:object_r:ssh_home_t:s0           153 Jan 14 21:47 .google_authenticator
-rw-------.  1 user user unconfined_u:object_r:ssh_home_t:s0      2602 Jan  3 18:15 id_rsa
-rw-r--r--.  1 user user unconfined_u:object_r:ssh_home_t:s0       572 Jan  3 18:15 id_rsa.pub
-rw-r--r--.  1 user user unconfined_u:object_r:ssh_home_t:s0      2078 Jan  9 20:33 known_hosts

(Note: I’m quite sure the initial context was unconfined_u:object_r:ssh_home_t ,but it is apparently changed after the first login).

Just to clarify, this will work for user/password authentication. The verification code will not be required for public key authentication. Can it be done? Yes. Does it make sense? Probably not.

# Bonus track: grace period

You may want to skip the 2FA for some time if you frequently login to the box. For example, you may want to request the verfication code once every hour. If that’s your case, just update /etc/pam.d/sshd like this:

auth required pam_google_authenticator.so secret=/home/${USER}/.ssh/.google_authenticator grace_period=3600

where grace_period is the number of seconds the 2FA will be ignored.

## Beyond ssh: 2FA for your graphical login

Chances are that you are happy with the above configuration, but what if you use RHEL/CentOS/Rocky or even Fedora as a daily driver? Can you use Google Authenticator for you graphical login? Of course you can but I’m not sure it is the most convenient.

Take into account that different services use different PAM modules. Documentation is not very clear so there is some trial and error here. In general, system-auth is mostly used by local services and password-auth by network services. But I don’t know for sure…

Let’s start by temporarily disabling SELiniux and modifying /etc/pam.d/system-auth:

Remove:

auth        sufficient        pam_unix.so

and add:

auth        requisite         pam_unix.so
auth        sufficient        pam_google_authenticator.so secret=/home/${USER}/.ssh/.google_authenticator

in its place. So far so good:

user@host$ su - user
Password: 
Verification code: 
user@host$

Unfortunately the very convenient grace_period does not work 😢

Jan 14 22:39:49 host su-l(pam_google_authenticator)[23132]: Accepted google_authenticator for user
Jan 14 22:39:49 host su-l(pam_google_authenticator)[23132]: debug: google_authenticator for host "(null)"
Jan 14 22:39:49 host su-l(pam_google_authenticator)[23132]: Failed to store grace_period timestamp in config

The grace period works storing the IP address and the timestamp of the last successful login in the secret file. Like this:

" LAST0 ::1 1642197329

(yes, ::1 is the IP address)

This time it is not a permissions or SELinux problem. My guess is grace period does not work with local authentication because the service does not pass the IP address. This may be a showstopper because no one wants to pick up the phone, start the app and enter the 6-digit 2FA every time your screen is locked after inactivity timeout.

Of course there would be also SELinux denials but grace period won’t work anyway.

By the way, graphical login could be configured adding:

auth    sufficient    pam_google_authenticator.so secret=/home/${USER}/.ssh/.google_authenticator grace_period=3600

to /etc/pam.d/gdm-password. But again the secret file can’t be updated with the IP and timestamp so the grace period does not work.

There will be a SELinux denial because SELinux will not allow gdm to write to the $HOME/.ssh directory. The verification code will work but the secret file update won’t (it would not work anyway because the host is ’null’).

If you want to use it anyway I would suggest to copy the same secret file to a directory outside .ssh. $HOME (the default ocation for .google_autenticator) would work but maybe a new standard location (i.e. $HOME/.config/google-authentiator/secret) would be better once all involved parties agree. Doing this everything should work except grace period no matter if SELinux is enforcing.

So to summarize, at least google-authenticator-libpam needs to be patched. If you are not happy you can try a YubiKey as an alternative.

## Caveats

Even if you have configured 2FA it is trivial for an attacker with physical access to your machine to bypass by booting in rd.break mode. Encrypt your filesystem, protect your GRUB and BIOS with passwords, etc. according to your risk.

While testing you can get locket out. In that case you can follow this guide and revert the changes in the screwed up files (instead of updateing the password).

## References