Automate SSH 2FA Hardening with Google Authenticator
Learn to automate SSH Two-Factor Authentication (2FA) hardening using Google Authenticator. This guide provides production-ready steps, Ansible examples, and trou...
Manual SSH 2FA Is a Security Gap and Operational Burden
Relying solely on SSH keys or passwords for server access is a significant security vulnerability. Even with strong SSH keys, a compromised workstation can expose all your access. Two-Factor Authentication (2FA) for SSH adds a critical layer of defense, typically by requiring a Time-based One-Time Password (TOTP) from an app like Google Authenticator.
Manually setting up pam_google_authenticator on dozens or hundreds of servers, then individually configuring each user, creates massive operational overhead and inconsistency. This manual process often leads to skipped deployments, misconfigurations, and, ultimately, a weaker security posture across your infrastructure.
Why SSH 2FA Automation Is Crucial
Deploying SSH 2FA with Google Authenticator presents several challenges:
- Fragmented Configuration: Integrating 2FA requires changes in
/etc/pam.d/sshdand/etc/ssh/sshd_config. Each file has specific syntax and order of operations. Manual changes are error-prone; a single typo can lock you out of the system. - User-Specific Secrets: Each user needs to generate their unique secret key and
.google_authenticatorfile. Distributing these securely and consistently for a large team is impractical without automation. - Permissions and Ownership: The
.google_authenticatorfile must have strict permissions (0400 or 0600) and be owned by the user. Incorrect permissions lead to “Failed to read” errors during login. - Lack of Rollback Strategy: Manual changes often lack an easy way to revert if something goes wrong. This increases the risk of lockout and downtime.
- Troubleshooting Complexity: When authentication fails, debugging involves checking multiple logs (
/var/log/auth.log), PAM configurations, SSH daemon settings, and user-specific files. This is tedious without a clear, repeatable process.
Automating SSH 2FA Hardening with Google Authenticator
Implementing SSH 2FA with Google Authenticator must be an automated process, especially in environments with many servers and users. This section details the necessary steps and automation strategies.
Prerequisites
You’ll need sudo privileges on your Linux servers. This guide uses pam_google_authenticator for TOTP functionality. Ensure your package manager is updated before installation.
1. Install libpam-google-authenticator
Install the PAM module on your Linux distribution.
For Debian/Ubuntu (v20.04+):
$ sudo apt update
$ sudo apt install -y libpam-google-authenticator
For CentOS/RHEL (v8+):
$ sudo dnf install -y google-authenticator
On CentOS/RHEL, the google-authenticator package primarily provides the pam_google_authenticator.so module. The google-authenticator command for interactive user secret generation (as used on Debian/Ubuntu) typically works differently or is less suitable for scripting on RHEL-based systems. We will address this distinction in the user secret automation section.
2. Configure PAM for SSH
Modify the /etc/pam.d/sshd file to integrate pam_google_authenticator.so. This instructs PAM to require a TOTP code. It’s safest to add this as a required step, after pam_unix.so (for password) or pam_sepermit.so (for keys), but before pam_deny.so.
An example of an original line might be:
@include common-auth
Add the following line. The nullok option allows users to skip 2FA if they successfully authenticate with a public key and haven’t set up 2FA yet. Remove nullok once 2FA is enforced for all users.
$ sudo sed -i '/@include common-auth/a auth required pam_google_authenticator.so nullok' /etc/pam.d/sshd
For strict enforcement, use required:
auth required pam_google_authenticator.so
For a smoother transition, you might use sufficient initially. This allows 2FA if configured, but does not strictly require it. Later, you can switch to required once all users are onboarded. Consult your system’s man pam_google_authenticator for the most accurate and current options.
3. Configure SSH Daemon (sshd_config)
Enable ChallengeResponseAuthentication and UsePAM in /etc/ssh/sshd_config. This tells the SSH daemon to use PAM for authentication challenges, including the TOTP prompt.
$ sudo sed -i 's/^ChallengeResponseAuthentication no/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config
$ sudo sed -i '/^UsePAM/c\UsePAM yes' /etc/ssh/sshd_config
$ sudo systemctl restart sshd
Always restart sshd after configuration changes. Open a new SSH session to test the changes. Crucially, keep your existing session open until you confirm success to avoid locking yourself out.
4. Automating User-Specific Google Authenticator Secrets
Automating user secret generation is where configuration management tools become essential. For a single user, you typically run $ google-authenticator interactively. For multiple users or servers, you need a robust strategy to generate these non-interactively and securely distribute the secrets.
The interactive $ google-authenticator command generates a QR code, a secret key, and emergency scratch codes. This secret is saved in ~/.google_authenticator. Automated secret generation means creating this file programmatically.
Configuration management tools like Ansible can deploy the PAM and SSHD configurations and then securely manage user secrets.
Example Ansible Playbook Snippet for User Setup:
# ansible/playbooks/setup_2fa.yml
---
- name: Configure SSH 2FA with Google Authenticator
hosts: your_servers
become: yes
vars:
# Example for specific users, replace with a dynamic user list
users_to_harden:
- john.doe
- jane.smith
tasks:
- name: Ensure libpam-google-authenticator is installed (Debian/Ubuntu)
ansible.builtin.apt:
name: libpam-google-authenticator
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Ensure google-authenticator is installed (RedHat)
ansible.builtin.yum:
name: google-authenticator
state: present
when: ansible_os_family == "RedHat"
- name: Configure PAM for SSHD 2FA
ansible.builtin.lineinfile:
path: /etc/pam.d/sshd
line: 'auth required pam_google_authenticator.so nullok'
insertafter: '@include common-auth'
state: present
notify: Restart sshd
- name: Ensure ChallengeResponseAuthentication is enabled in sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^ChallengeResponseAuthentication'
line: 'ChallengeResponseAuthentication yes'
state: present
notify: Restart sshd
- name: Ensure UsePAM is enabled in sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^UsePAM'
line: 'UsePAM yes'
state: present
notify: Restart sshd
- name: Generate Google Authenticator secrets for users (Debian/Ubuntu only, non-interactive)
ansible.builtin.shell: |
echo "y" | google-authenticator -t -d -f -r 3 -R 30 -w 3 -c -D -q --secret-file=/home/{{ item }}/.google_authenticator
args:
creates: /home/{{ item }}/.google_authenticator
become_user: "{{ item }}"
loop: "{{ users_to_harden }}"
register: ga_output_debian
when: ansible_os_family == "Debian"
- name: Display Google Authenticator output for Debian/Ubuntu users
ansible.builtin.debug:
msg: "User {{ item.item }}: Secret details are in /home/{{ item.item }}/.google_authenticator. Extract the secret from this file and provide it to the user securely. You can also generate a QR code from the secret."
loop: "{{ ga_output_debian.results }}"
loop_control:
label: "{{ item.item }}"
when: item.changed and ansible_os_family == "Debian"
- name: Set correct permissions on .google_authenticator file
ansible.builtin.file:
path: "/home/{{ item }}/.google_authenticator"
owner: "{{ item }}"
group: "{{ item }}"
mode: '0400'
loop: "{{ users_to_harden }}"
- name: WARNING - Manual secret generation for RedHat systems
ansible.builtin.debug:
msg: "For RedHat-based systems, the 'google-authenticator' command often behaves differently for non-interactive secret generation. It's recommended to manually run 'google-authenticator' as each user or pre-generate base32 secrets and push the ~/.google_authenticator file using a secure method. Ensure the file has 0400 permissions and is owned by the user."
when: ansible_os_family == "RedHat"
handlers:
- name: Restart sshd
ansible.builtin.service:
name: sshd
state: restarted
The non-interactive generation of secrets (using flags like -t -d -f -r 3 -R 30 -w 3 -c -D -q) will create the .google_authenticator file on Debian/Ubuntu systems. After generation, you need a secure method to extract the secret keys or QR codes and provide them to users. Examples include using a secure internal portal, a one-time secret sharing tool, or directly via a secure chat after extracting the secret from the generated file. Never email or chat plain secret keys in an unsecured manner.
For RedHat systems, if direct non-interactive secret generation is problematic with the google-authenticator command, consider generating the base32 secret key externally (perhaps with a script) and then pushing a pre-formatted .google_authenticator file to each user’s home directory. This file should contain lines like:
SECRET_KEY
" RATE_LIMIT 3 30
" WINDOW_SIZE 3
Replace SECRET_KEY with the actual base32 secret.
Troubleshooting Scenarios
Even with automation, issues can arise.
”Failed to read “.google_authenticator""
This error usually indicates a permissions or missing file issue.
-
Permissions: The
~/.google_authenticatorfile must be owned by the user and have strict permissions, such as0400or0600.$ ls -l ~/.google_authenticator # Expected output: -r-------- 1 user user ... $ chmod 0400 ~/.google_authenticator
* **Missing File:** The user might not have run `google-authenticator` yet, or the automated process failed to create the file. Ensure the file exists in the user's home directory.
* **Home Directory Permissions:** If the user's home directory has incorrect permissions, for example, if it's world-writable, `pam_google_authenticator` might refuse to read the secret file for security reasons. `chmod 755 /home/username` is a safe default.
#### PAM Authentication Failures
Check `/var/log/auth.log` (Debian/Ubuntu) or `/var/log/secure` (CentOS/RHEL) for detailed PAM errors.
```bash
$ sudo tail -f /var/log/auth.log | grep sshd
Look for lines containing pam_google_authenticator or sshd failures. Common issues include an incorrect PAM stack order or syntax errors in /etc/pam.d/sshd.
”Too many authentication failures”
This message indicates the SSH client has attempted too many authentication methods without success. It can be caused by:
- Incorrect
sshd_config: IfMaxAuthTriesis set too low while you have multiple authentication steps (password, 2FA). IncreaseMaxAuthTriesin/etc/ssh/sshd_configif necessary, but avoid setting it excessively high as this reduces security. - Incorrect TOTP codes: Users repeatedly entering wrong codes from their authenticator app.
- Time Drift: The server’s time and the Google Authenticator app’s time might be out of sync. Use
ntpdateorchronyto keep server time accurate. A drift of more than 30 seconds can cause authentication failures.
Connectivity Issues Post-Setup (Locked Out)
If you find yourself locked out after making changes, you need a recovery plan:
- Keep an emergency root shell or console session open: Always test changes in a new SSH session while keeping an existing, authenticated one open. This is your immediate fallback.
- Use a console or KVM access: Access the server directly through your cloud provider’s console or a physical KVM to revert changes to
/etc/pam.d/sshdor/etc/ssh/sshd_config. - Restore from backup: If you use configuration management, you can restore previous versions of the config files. This emphasizes why automation provides a clear path to rollback.
Prevention: Best Practices for Secure and Automated 2FA
To avoid these problems and maintain a strong security posture, follow these best practices:
- Configuration Management Is Key: Always deploy
pam_google_authenticatorandsshd_configchanges using tools like Ansible, Puppet, Chef, or SaltStack. This approach ensures consistency, reduces manual error, and provides a clear audit trail of all changes. - Secure Secret Distribution: Use purpose-built secret management systems, such as HashiCorp Vault, AWS Secrets Manager, or internal secret sharing tools, to distribute 2FA secret keys to users. Never rely on insecure channels like email or chat for distributing these critical secrets.
- Regular Audits: Periodically audit
/etc/pam.d/sshd,/etc/ssh/sshd_config, and user.google_authenticatorfile permissions across your fleet. This helps identify and correct unauthorized modifications or misconfigurations that could compromise security. - Time Synchronization: Ensure all your servers use NTP or Chrony for accurate time synchronization. Time drift is a common cause of TOTP failures and can be difficult to diagnose without proper server timekeeping.
- User Revocation Procedures: Develop a clear, automated process for revoking a user’s 2FA access if their device is lost or they leave the organization. This typically involves deleting their
.google_authenticatorfile from affected servers. - Emergency Access: Maintain a highly restricted, separate “break glass” emergency access mechanism. This could be a locked-down SSH key with specific IP restrictions that bypasses 2FA. Reserve this for critical, documented emergencies only, and ensure its use is heavily logged and reviewed.
Related articles
Stay up to date
Get DevOps tips, tutorials, and guides delivered to your inbox.