Setting up two-factor authentication on FreeBSD

I typically utilize public key authentication when connecting via SSH to However, there are times when I’m away from a device which has my private key and need access to my server. I reluctantly enabled password authentication for those occasions, but after enabling two-factor authentication for most of the services that I use regularly, I wanted to do the same for my own server.

Setting it up turned out to be more difficult than expected. FreeBSD includes an OPIE module, but it doesn’t integrate with the existing two-factor app I use to generate codes for GitHub, Google, etc. I wanted something that supported the HOTP/TOTP algorithm so that I could use it with Google Authenticator. I then discovered OATH Toolkit.

The documentation for pam_oath provides some cursory details on how to set up pam_oath. Unfortunately the instructions will result in an insecure configuration which will produce predictable authentication codes. Here I’ll outline how to configure pam_oauth on FreeBSD for a single user in the wheel group, allowing access from all other users without OATH.


SSH on FreeBSD utilizes PAM to authenticate users. The default SSH configuration enables ChallengeResponseAuthentication and disables PasswordAuthentication, which is required for pam_oauth to function properly. If ChallengeResponseAuthentication is disabled and PasswordAuthentication enabled while UsePAM is on, two-factor authentication will not function.


PAM is configured per service. The PAM configuration for ssh is in /etc/pam.d/sshd.

We are concerned with the entries pertaining to the auth facility, located at the top of the file. Mine looked like:

auth sufficient no_warn no_fake_prompts
auth requisite no_warn allow_local
#auth sufficient no_warn try_first_pass
#auth sufficient no_warn try_first_pass
auth required no_warn try_first_pass

I removed the pam_opie modules and added the OATH module:

auth requisite no_warn try_first_pass
auth sufficient deny luser
auth required /usr/local/lib/security/ usersfile=/usr/local/etc/users.oath

I also added the pam_group module, which short-circuits OATH for normal users (those not in the wheel group). This works by rejecting any user in the wheel group, forcing PAM to evaluate the next module. If a user is not in the wheel group, the pam_group breaks out of the auth chain, skipping the pam_oath module.

Changing pam_unix to requisite ensures that a mistyped password won’t prompt for an authentication code until the correct UNIX password is entered.


Once PAM has been configured, it is necessary to configure the pam_oath module itself. The usersfile argument to pam_oath must point to a file that contains an entry for each OATH user. The entries are simple – the token type, username, optional password, and secret.

I generated the secret with SECRET=$(head -c 1024 /dev/urandom | openssl sha1).

I then added an entry for myself to /usr/local/etc/users.oath, specifying:

HOTP/T30/6 mhoran - $SECRET

This file must be owned by root and mode 600:
chmod 600 /usr/local/etc/users.oath
chown root /usr/local/etc/users.oath

While the OATH configuration file expects a hexadecimal secret, the Google Authenticator app expects a Base32 encoded secret. The OATH Toolkit may be used to transform the hexadecimal secret to Base32: SECRET32=$(oathtool -v --totp $SECRET | grep Base32 | awk '{ print $3; }').

Once the Base32 secret has been procured, the last step is to set up a token generator. I used the Google Charts API to generate a QR code, which can be scanned into the Google Authenticator app. While insecure, the following command will return an HTTPS URI of a QR code from the Google Charts API:

echo "|0&cht=qr&chl=otpauth://totp/$USER@$HOST%3Fsecret%3D$SECRET32"

Alternatively the Base32 encoded secret can be manually entered into any authenticator app which supports HOTP/TOTP with the token type set to time based, or an otpauth URI provided to a trusted QR code generator.


Once it’s all set up, SSH in and you should be prompted for a “One-time password” when logging in as a user in the wheel group:


If the module has been properly configured and the token generator is working, login should succeed.


Post a Comment

You must be logged in to post a comment.
%d bloggers like this: