Setting Up 2FA on SELinux
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.