Setting Up 2FA on SELinux

Updated:

Let’s configure Two-Factor Authentication (2FA) using Google Authenticator on SELinux.

Installing Google Authenticator

At the time of writing, Google Authenticator is not available in the EPEL release repository. Therefore, we’ll install it directly from the package URL as follows:

sudo dnf install -y https://dl.fedoraproject.org/pub/epel/10/Everything/x86_64/Packages/g/google-authenticator-1.11-1.el10_1.x86_64.rpm
sudo dnf install -y qrencode

The qrencode package is used to generate 2FA QR codes.

Now, the command to set up your 2FA key is:

google-authenticator -t -d -f -r 3 -R 30 -W -l "$(whoami)@$(hostname)" -i "SSH (Rocky10)"

The options explained:

  • -t, --time-based: Use time-based TOTP (Time-based One-Time Password)
  • -d, --disallow-reuse: Prevent reuse of previously used tokens
  • -f, --force: Force overwrite of existing key without prompting
  • -r, --rate-limit=N / -R, --rate-time=M: Allow N login attempts within M seconds
  • -W, --minimal-window: Disable token validity window feature
  • -l, --label: Token label/name
  • -i, --issuer: Issuer name

Scan the generated QR code with your authenticator app to complete the 2FA setup.

Configuring sshd and PAM

Now we need to configure sshd to use 2FA. The key is to add PAM (Pluggable Authentication Module), which manages 2FA, to the SSH authentication process. First, add the following line to the PAM configuration file:

# /etc/pam.d/sshd
auth required pam_google_authenticator.so

Then modify the following settings in the sshd configuration:

# /etc/ssh/sshd_config
UsePAM yes
KbdInteractiveAuthentication yes
PasswordAuthentication no      
PubkeyAuthentication yes
AuthenticationMethods keyboard-interactive:pam
PermitRootLogin no

With this configuration, the PAM authentication process will prompt for both 2FA and password, requiring both to be correct for login. If you want to use public key authentication instead, first remove this line from the PAM configuration:

auth    substack      password-auth

Then add publickey to the AuthenticationMethods in /etc/ssh/sshd_config:

AuthenticationMethods publickey,keyboard-interactive:pam

You might think you could allow either publickey OR password authentication followed by 2FA using:

AuthenticationMethods publickey,keyboard-interactive:pam password,keyboard-interactive:pam

However, this doesn’t work as intended. Since both password and PAM use keyboard-interactive, the keyboard input used for password authentication gets passed directly to PAM and is used for verification code authentication.

You might think adding password-auth back to PAM would solve this, but that would require password authentication even after publickey authentication, which is undesirable.

# /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive:pam keyboard-interactive:pam
# /etc/pam.d/sshd
auth    substack      password-auth

Therefore, we need to modify PAM to skip password-auth and proceed directly to 2FA when publickey authentication succeeds. To accomplish this, we need to create and execute the following shell script:

# /usr/local/bin/is_pubkey_ssh.sh
#!/bin/bash

if [ "$PAM_TYPE" = "auth" ] && [ "$PAM_SERVICE" = "sshd" ]; then
  if grep -qa 'SSH_AUTH_INFO_0=publickey' /proc/$$/environ; then
    exit 0
  elif grep -qa 'SSH_CONNECTION' /proc/$$/environ; then
    exit 1 
  fi 
fi

Then configure PAM as follows:

# /etc/pam.d/sshd
auth [success=1 default=ignore] pam_exec.so quiet /usr/local/bin/is_pubkey_ssh.sh
auth substack password-auth
auth required pam_google_authenticator.so 
auth include postlogin

This way, when publickey authentication succeeds, password-auth will be skipped and the authentication will proceed directly to 2FA.

Troubleshooting

Configuration Override Issues

It’s not recommended to apply the above sshd configuration changes immediately. If you misconfigure and reload sshd, you might lose all remote access to your server. To prevent this, use sudo sshd -t to test your configuration. This command will identify any configuration errors. In my case, I kept getting errors about keyboard-interactive being disabled, which was caused by an overriding line in /etc/ssh/sshd_config.d/50-redhat.conf:

# /etc/ssh/sshd_config.d/50-redhat.conf
ChallengeResponseAuthentication no  # Remove this line or change to yes

After fixing this, reload the configuration with sudo systemctl reload sshd. Be careful not to lose your existing session. Open a new terminal and test the connection to verify that the sshd configuration is working correctly before closing your existing session.

Google Authenticator Permission Issues

Even after completing the configuration above, you’ll encounter Permission denied errors during Google Authenticator authentication:

Oct 7 21:08:19 user sshd(pam_google_auth)[291243]: Failed to update secret file "/home/user/.google_authenticator": Permission denied 

This issue arises from a conflict between SELinux policies and Google Authenticator’s secret file management. Google Authenticator reads the ~/.google_authenticator file when attempting authentication. To prevent token reuse, it creates a temporary file, moves the authentication secret to it, and then moves it back. The problem is that the SSH daemon doesn’t have write permission to files in the home directory. This issue has been discussed since 2011. The proposed solution is to change the storage location of Google Authenticator’s secret files and grant the SSH daemon write permission to that path.

I chose to store secret files in /var/lib/google-authenticator, keeping their ownership as root while granting write permission to the SSH daemon.

First, generate the authentication secret in the new location:

sudo google-authenticator -t -d -f -r 3 -R 30 -W -l "$(whoami)@$(hostname)" -i "SSH (Rocky10)" -s /var/lib/google-authenticator/${USER}

Note that sudo privileges are required.

Next, prepare the directory and set permissions:

sudo mkdir -p /var/lib/google-authenticator
sudo semanage fcontext -a -t var_auth_t '/var/lib/google-authenticator(/.*)?'
sudo restorecon -Rv /var/lib/google-authenticator

sudo chown root:root /var/lib/google-authenticator
sudo chmod 755 /var/lib/google-authenticator
sudo chmod 600 /var/lib/google-authenticator/${USER}

Finally, update the PAM configuration to use the new secret file location:

# /etc/pam.d/sshd
auth required pam_google_authenticator.so secret=/var/lib/google-authenticator/${USER} user=root

What seemed like a simple 2FA setup is now complete. Since I don’t have many local machines accessing the server, I configured 2FA authentication to use publickey instead of password.