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.

Setting up Postfix and Dovecot to play nicely with mutt, mbox, Maildir and FreeBSD

I’m one of two people I’m aware of who still runs their own e-mail servers. I do this for a multitude of reasons, mostly because I love mutt and nothing else quite stacks up to it. Running a local mail server with mutt reading a local inbox is relatively simple. I run Postfix and it gets the job done with relatively little configuration. However, if you’d like to check mail remotely, and not rely on ConnectBot to SSH into your ARP Networks VPS, you’ll have to venture into the world of IMAP servers.

To check mail on the go, I use K-9 Mail. As an Android user, the stock e-mail app leaves much to be desired, and K-9 brings handy features like IMAP IDLE support, which provides push e-mail support.

On the server side, I decided to go with Dovecot. Dovecot is a relatively simple IMAP server which has great Postfix support. I originally went with Dovecot because it integrates with Postfix SASL, and is a great alternative to Cyrus SASL. SASL is important in the context of SMTP servers because it allows remote clients to send e-mail authenticated through your server, therefore not having to be an open relay in order to send e-mail on the go.

As a FreeBSD user, I’ll be providing FreeBSD specific steps for getting Postfix, Dovecot, mutt and K-9 mail playing nicely together. The process isn’t too involved, but I’ll explain the steps and what I know about them along the way.

Getting everything installed is relatively simple:

portsnap fetch update
portmaster mail/postfix mail/dovecot2 mail/mutt

This will install the latest version of Postfix, Dovecot 2 and mutt (non-devel). It’s important to note the dovecot2, as the mail/dovecot port is for Dovecot 1.

I also recommend installing mail/dovecot-sieve as an alternative to procmail. I found procmail locking support to be lacking on FreeBSD, and this is something that is quite important when you have multiple processes accessing your inbox, if you’re using mbox. This is of course less concern with Maildir, however, Sieve is a great alterntive to procmail and can be configured from supported third-party plugins for various mail clients.

make config will prompt with a few questions for the mail/postfix and mail/dovecot2 packages. There are a couple of options for each with are important. For mail/dovecot2, I recommend enabling SSL, otherwise your credentials will be sent in the clear across the Internet. If you’re using a non-PAM based authentication mechanism, other authentication schemes can be used such as CRAM-MD5, however this will only protect your password. All of your mail will still be sent unencrypted. For mail/postfix, you’ll want to be sure to enable the Dovecot 2.x SASL authentication method and SSL/TLS support.

There’re a few configuration changes which need to be made to get everything playing together nicely. I tinkered with this configuration for about a year or so before finally getting it right. For a long while, I noticed that I would sometimes delete a message from my inbox via mutt, but then it would reappear in K-9 mail, and vice versa. I didn’t really mind this at first, but I was afraid that this would ultimately lead to a corrupt mailbox, which it did. I eventually figured out the right combination of settings which got the combination of local mail and IMAP playing nicely, but switching to Maildir instead of mbox will also do the trick.

There are two options for local mail delivery with Dovecot and Postfix. One is dovecot-lda, a simple solution for a small installation. The other is Dovecot’s built-in LMTP sever. Each has advantages and disadvantages. dovecot-lda is relatively simple, and similar in concept to procmail. It’s a process that runs, via Postfix mailbox_command, for local mail delivery. LMTP definitely makes more sense on larger installations, as it runs as a daemon and hooks in with Postfix via mailbox_transport. However, configuration is non-trivial, and on my installation it wasn’t worth the effort.

There are a few caveats with dovecot-lda. If using mbox, dotlocking for /var/mail inbox spools won’t work. This is because dovecot-lda runs as the user being delivered to, and this user typically isn’t in the mail group, and can’t create a dotlock file in /var/mail. One option is to enable the sticky bit on /var/mail, but there are various security implications with this approach. I’ve switched to Maildir, which is better for a variety of reasons. However, if you are using mail spools in shared directories which can’t be written by the user (read: you’re not using Maildir), you’re going to have to set mbox_read_locks = flock and mbox_write_locks = flock in /usr/local/etc/dovecot/conf.d/10-mail.conf. The issues with message deletion were solved by setting mbox_dirty_syncs = no and mbox_lazy_writes = no, which were both yes by default in my installation. Again, these changes are not necessary if you’re running Maildir.

The Dovecot LMTP server is a great alternative to dovecot-lda for large installations. It also plays nicely with /var/mail inbox spools, as the mail_privileged_group setting gives the LMTP process escalated permissions when necessary. However, there is a caveat with the LDA and postifx virtual aliases, which is that the LMTP daemon will look for the fully qualified username, e.g. root@hostname instead of just root. If you’re using the default PAM database, this will of course fail. If you’re using the LMTP transport with mbox, you’ll also need to set client_limit = 1 option inside the service lmtp block in /usr/local/etc/dovecot/conf.d/10-master.conf. As I’m not running LMTP, setup is outside the scope of this post, however all the necessary documentation can be found on the Dovecot wiki.

The key for setting up Maildir, or mbox delivery should you prefer, is the mail_location setting in /usr/local/etc/dovecot/conf.d/10-mail.conf. I have this option set to mail_location = maildir:~/Maildir. As the Maildir is stored in the home directory, there are no shared locking issues to worry about, as the user has control over all the files that need to be created. As stated earlier, if you use an alternative configuration with mbox, mail_location = mbox:~/mail/:INBOX=/var/mail/%u, you’re going to have to be concerned with locking, dirty and lazy writes.

Once locking and mailbox delivery are all set up, you’ll have to tell Dovecot to do its business. You must enable the IMAP protocol by setting protocols = imap in /usr/local/etc/dovecot/dovecot.conf. The default configuration will use PAM authentication by default, which requires setup of the /etc/pam.d/dovecot service. My file looks like this:

auth    required
account required

If you’ve decided to go down the route of CRAM-MD5 authentication, you’re not going to be able to use PAM authentication, as the password database has to be stored in the clear.

To setup Postfix SASL authentication, you must set up a Dovecot auth listener. This can be done by adding the following to /usr/local/etc/dovecot/conf.d/10-master.conf, inside the service auth block:

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666

SSL configuration is outside of the scope of this post, however an ssl_cert and ssl_key must be set in /usr/local/etc/dovecot/conf.d/10-ssl.conf. smtpd_tls_cert_file must be set in /usr/local/etc/postfix/ as well.

To get the Dovecot LDA up and running, you must set postmaster_address in /usr/local/etc/dovecot/conf.d/15-lda.conf. I’ve also set lda_mailbox_autocreate = yes so that saving mail to a new mailbox creates the mailbox automatically. To enable the sieve plugin for the Dovecot LDA, don’t forget to add sieve to the mail_plugins under protocol lda. This is the only thing that needs to be set to enable sieve, with the magic coming (by default) from ~/.dovecot.sieve.

My ~/dovecot.sieve is relatively simple. Here’s an excerpt:

require ["fileinto", "envelope"];

if header :contains "List-Id" "" {
  fileinto "freebsd";
} elsif header :contains "List-Id" "" {
  fileinto "void";

The path to the user’s dovecot.sieve can be changed in /usr/local/etc/dovecot/conf.d/90-sieve.conf.

Once these changes have been made, Dovecot is ready to go. Just set dovecot_enable="YES" in /etc/rc.conf and start it up via /usr/local/etc/rc.d/dovecot start. Tail /var/log/maillog to check for any configuration errors.

I’m using Postfix virtual aliases for local mail delivery. This allows me to have multiple email accounts at domains I host, all delivered to various local system accounts. It’s a relatively simple solution for a relatively simple setup. Virtual aliases are out of the scope of this post, but I’ll assume you’ve got some sort of local delivery happening with Postfix.

To hook up Postfix delivery to Dovecot, if going down the route of dovecot-lda, simply add mailbox_command = /usr/local/libexec/dovecot/dovecot-lda to the /usr/local/etc/postfix/ This will have a similar effect to the inline example of using procmail for local delivery.

If going down the route of LMTP, you’ll not only need to get a working userdb mapping for incoming email addresses (virtual aliases are delivered via LMTP at their fully qualified name, e.g. root@hostname), but also set mailbox_transport = lmtp:unix:private/dovecot-lmtp in /usr/local/etc/postfix/

I also recommend setting mailbox_size_limit = 0 and message_size_limit = 0 in /usr/local/etc/postfix/, else you may experience issues with local delivery. Postfix applies limits to the size of a mailbox even when delivered via dovecot-lda. I ran into this issue with mbox delivery, though I’m not sure if it’s also an issue with maildir.

To get Postfix playing nicely with Dovecot SASL, add the following to /usr/local/etc/postfix/

smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_authenticated_header = yes

smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination

You’ll want to merge smtpd_recipient_restrictions with any other custom restrictions you may already have in place. The key is permit_sasl_authenticated, which bypasses reject_unauth_destiontion, preventing your mail server from becoming an open relay.

By default, SASL will be enabled on port 25, accepting plaintext auth (your username and password) across the Internet. I recommend setting up SSL, as with Dovecot. Once you’ve got this set up, you should disable plaintext authentication. This is done by default in Dovecot, except for localhost. To do this in Postfix, you must set smtpd_tls_auth_only = yes. Then you’ll need to enable an alternative, secure port for accepting SASL auhtenticated mail. I’ve set up the submission port (587) to do this in /usr/local/etc/postfix/

submission inet n       -       n       -       -       smtpd
  -o smtpd_tls_security_level=may
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

Once this is all set, you should be ready to disable FreeBSD sendmail in favor of Postfix. Add the following to /etc/rc.conf:



At this point, you should be ready to start up Postfix via /usr/local/etc/rc.d/postfix. Again, tail /var/log/maillog for any error messages.

Once both Dovecot and Postfix are successfully up and running, you should be able to get your favorite IMAP client connected. You’ll be unable to check mail via IMAP by default without setting up SSL. However, if you don’t care about security, you can set disable_plaintext_auth = no in /usr/local/etc/dovecot/conf.d/10-auth.conf. Setting up SSL is really simple and totally worth it. I use StartCom Free SSL certificates, which work on every device I own.

K-9 mail plays nicely with this configuration, and Dovecot supports IMAP IDLE out of the box. I’m loving push IMAP to my Galaxy Nexus, and I get all the benefits of Gmail without the pain of setting up OfflineIMAP or something similar.

FreeRADIUS on FreeBSD and OpenLDAP

Instead of relying on PAM or /etc/passwd for authentication and authorization, I decided to store account information in an OpenLDAP database. Of course I could have used NIS or flat file databases, but OpenLDAP proved to be the best solution for my situation.

sudo portinstall net/openldap23-server

This will install the OpenLDAP 2.3 client and server.

sudo portinstall net/freeradius

This will install the FreeRADIUS server. To enable the LDAP backend, check the LDAP option.

Once FreeRADIUS has finished compiling, OpenLDAP can be configured.

First, the RADIUS LDAP schema must be copied to the OpenLDAP schema directory.

sudo cp /usr/local/share/freeradius/openldap.schema /usr/local/etc/openldap/radius.schema

Next, slapd must be configured.

sudoedit /usr/local/etc/openldap/slapd.conf

The RADIUS schema must be added to the include section. The Cosine schema provides the account objectclass.

include /usr/local/etc/openldap/schema/core.schema
include /usr/local/etc/openldap/schema/cosine.schema

You must also configure at least one database backend.

database bdb
suffix "dc=matthoran,dc=com"
rootdn "cn=Manager,dc=matthoran,dc=com"
rootpw secret
directory /var/db/openldap-data
index objectClass eq
index uid eq

Indexing the objectclass and uid will speed up LDAP queries.

To enable slapd on FreeBSD, add the following to /etc/rc.conf and run the init script.

slapd_flags="-h ldap://"
sudo /usr/local/etc/rc.d/slapd start

Note that this will cause slapd to listen on localhost only.

The ldapadd tool is used to add entries to the LDAP database. The ldapmodify command is used to modify entries in the LDAP database. Both tools take a number of arguments as well as an LDIF file.

dn: dc=matthoran,dc=com
objectclass: dcObject
objectclass: organization
o: Matt Horan
dc: matthoran

dn: cn=Manager,dc=matthoran,dc=com
objectclass: organizationalRole
cn: Manager

If not saved to a file, the above could be fed to ldapadd through stdin.

ldapadd -x -D "cn=Manager,dc=matthoran,dc=com" -W base.ldif

The -x flag causes ldapadd to use simple bind instaed of SASL. The -D flag indicates the distinguished name used to connect to the database and the -W flag causes ldapadd to prompt for simple authentication.

Now the first user may be added to the database.

objectclass: account
objectclass: simpleSecurityObject
objectclass: radiusprofile
uid: test
userPassword: {SSHA}53jF+nBYXuouGDSpKNaUIvOkyxCCEsah

ldapadd -x -D "cn=Manager,dc=matthoran,dc=com" -W test.ldif

This will add a user test with password test to the database. The slappasswd command may be used to generate hashed passwords. The default hash algorithm is SSHA and can be changed with the -h flag.

The ldapsearch command may be used to ensure that the above entry was successfully added to the database.

ldapsearch -x -b "dc=matthoran,dc=com" "(uid=test)"

This should return an LDIF identical to the content of test.ldif.

Now that the LDAP database contains a test user, FreeRADIUS can be configured.

Since my FreeRADIUS server is only performing AAA for EAP clients, there are only two parts of the default configuration file that need to be changed. Many of the others can be slimmed down or completely removed.

To get FreeRADIUS to talk to the LDAP server, the following LDAP configuration options must be changed, assuming that slapd is listening on localhost, that your rootdn is cn=Manager,dc=matthoran,dc=com and that your rootpw is secret.

server = "localhost"
identity = "cn=Manager,dc=matthoran,dc=com"
password = secret
basedn = "dc=matthoran,dc=com"
password_attribute = userPassword
set_auth_type = no

I commented out access_attr so that all users were allowed access, added password_attribute so that the PAP module can extract the userPassword attribute and set set_auth_type to no so that the PAP module will handle authentication.

Now that the LDAP module has been configured, the authorization module must be told to use LDAP for authorization. To do so, just uncomment the ldap line from the authorization section.

PAP is last in the default authorization chain. The default users file will set AuthType = System which will cause authentication to fail. Setting set_auth_type = yes in the LDAP configuration section will not solve this problem as the files database is checked before LDAP due to the order of the modules in the authorization section and would also require an additional LDAP bind. As I am not using the local passwd database for authorization or authentication, I commented out the files module in the authorization section. This will prevent Auth-Type from being set to System. The System Auth-Type may also be commented out in the Authorization section since it will not be used.

Now that the core is configured, EAP can be configured. By default, EAP is enabled, but the default configuration won’t get you too far.

I decided to use PEAP with GTC, which supports encrypted passwords in the database. Because incoming EAP messages do not specify which EAP type they are using, the default must be set for the EAP module. Be sure that you change the default EAP type for the EAP module and not PEAP.

sudoedit /usr/local/etc/raddb/eap.conf

default_eap_type = peap

The TLS and PEAP configuration sections are commented out by default. Because PEAP relies on TLS to set up a secure channel, the TLS module must be configured. The following configuration options must be uncommented:

private_key_password = whatever
private_key_file = ${raddbdir}/certs/cert-srv.pem
certificate_file = ${raddbdir}/certs/cert-srv.pem
CA_file = ${raddbdir}/certs/demoCA/cacert.pem
dh_file = ${raddbdir}/certs/dh
random_file = ${raddbdir}/certs/random

This configuration is sufficient for testing. Be sure to replace the distributed certificates with a signed certificate in a production environment.

If GTC is to be used as the innermost PEAP protocol, the default_eap_type must be set to gtc for the PEAP module. Be sure that you are setting default_eap_type for the PEAP module and not for the EAP module.

default_eap_type = gtc

Now that FreeRADIUS has been configured, it is time to start up the server.

sudo /usr/local/etc/rc.d/radiusd start

The radtest command line tool provides a quick and easy way to make sure that RADIUS can authenticate users. The default clients file includes an entry for localhost with the shared key testing123. Be sure to change this in a production environment.

radtest test test localhost 10 testing123

The first and second arguments to radtest are the username and password of the test user, respectively. radtest is connecting to localhost. The fifth option is the NAS-Port, used for accounting. The final argument is the shared key.

If you do not receive an Access-Accept packet, run FreeRADIUS from the command line in debug mode.

sudo /usr/local/etc/rc.d/radiusd stop
sudo radiusd -X

Now you can go ahead and test EAP interactively. I will not go into how to configure your NAS device to communicate with the RADIUS server. Make sure that you have an entry in /usr/local/etc/raddb/clients.conf for the NAS device.