Configuration notes for a Ubiquiti UniFi setup


Ubiquiti make some shiny WiFi gear that falls in to the “Enterprise Lite” space – not quite as robust as Cisco Meraki or Aerohive gear, but also nowhere near as expensive. Here are some notes I took while setting up my home WiFi with two UniFi UAP-AC-PROs and the UniFi Controller running in the cloud:

UniFi Controller

Get the Controller up and running before anything else. The easiest way to do this would probably have been to get a UniFi Cloud Key to run the Controller locally, then manage it using a Ubiquiti account – but seeing as I was too cheap to buy a Cloud Key, and I already have a server that I’m using for other things – it made sense for me to run the Controller on that server. I mostly followed the instructions for a Debian based UniFi Controller, with the following gotchas:

  • The firewall instructions open more ports than are absolutely necessary, and
  • The firewall instructions assume you’re crazy and want to configure iptables by hand, rather than using something like ufw

Another thing to keep in mind: the ports that the UniFi Controller use will cause problems if you’re also running Tomcat on the same server (or plan to do so in the future).

I decided to change the ports for the UniFi Controller – in the following configuration file, add the following two lines (don’t uncomment the lines towards the top of the file – as I understand it, you can’t have whitespace or comments after configuration parameters):



Restart the Controller:

sudo systemctl restart unifi


As I’m running the UniFi Controller out on them Internets and have no intention of accessing the Controller via the web on port 9443, the collection of ports I need to open are:

  • TCP/9080 – the port that the APs use to communicate with the Controller
  • TCP/8880 – used by the guest portal (could be dropped?)
  • TCP/8843 – also used by the guest portal (could also be dropped?)

I’m not including the following ports, for the following reasons:

  • UDP/3478 – used for STUN by the UniFi VoIP software
  • TCP/8081 – I can’t see a good reason to leave a “shutdown” port open
  • TCP/9443 – I’m running a reverse proxy for the UI (see below), so it’s not needed
  • UDP/100001 – only needed when the Controller is in the same subnet as the APs

Because I don’t hate myself, I didn’t attempt to configure iptables by hand. Here’s my ufw application file:


title=UniFi Controller
description=Ubiquiti UniFi Controller

Once the above file has been created, enabling the firewall rules couldn’t be simpler (you still need to ensure ufw is otherwise correctly configured first):

sudo ufw allow unifi

Apache reverse proxy

The UniFi Controller comes secured with a self-signed SSL certificate, and while it’s possible to change the Controller UI port to 443 and install a regular SSL certificate – it’s also a complete pain in the ass (and/or impossible if you’re already running a webserver). Instead, I set up a reverse proxy in Apache to handle the redirect and make it much, much easier to manage the SSL stuff. Here’s my Apache config file for the relevant virtualhost:


<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName unifi.controller.fqdn
CustomLog /path/to/access.log combined
ErrorLog /path/to/error.log
SSLProxyEngine On
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
ProxyPreserveHost On
ProxyPass / https://unifi.controller.fqdn:9443/
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf

Couple notes:

  • No need for a DocumentRoot – I’ve set this site up purely as a redirect, but you may want to do things differently
  • All of the SSLProxy.. lines are required – this is to avoid problems with the self-signed certificate on the Controller
  • The Include /etc/letsencrypt.. line – I’m using a Let’s Encrypt certificate, so this line is standard (your SSL configuration lines will probably be different if you’re using a different CA)

Once the above is up and running, you should be able to access the UniFi Controller at https://unifi.controller.fqdn/. Run through the setup wizard, but I’d recommend setting up another site (separate to “Default”) for the first site if you plan to have several sites managed by the Controller.

Setting up the APs

Try though I might, I couldn’t get the various versions of the UniFi Discovery Tool to work at all. I gave up after an hour – save yourself the grief and configure the inform URL by SSH’ing in to each AP after it picks up a DHCP lease (default user/pass is ubnt/ubnt) with the following command:

set-inform http://unifi.controller.fqdn:9080/inform

Wait a few seconds, then log in to the Controller and complete the “adoption” process. After your new AP is adopted and assigned to a site, go back to the SSH console and re-run the same command:

set-inform http://unifi.controller.fqdn:9080/inform

This saves the inform URL in case the AP reboots, etc.

Other Config Notes

Beyond the standard WiFi configuration, I also have the following items set for each AP:

  • Minimum RSSI set to -75 dBm (-80 dBm is a good starting point for experimentation)
  • Band Steering set to Prefer 5G
  • Airtime Fairness set to On

And the following set in the site configuration:

  • Enable status LED unchecked

If you ever need to ssh back in to the APs, the default username and password will not work. The new username and password are shown in the Controller – go to Settings and select the site the APs are connected to – the username and password are listed at the bottom of the page.

SpamAssassin sa-learn cron script for virtual users

Because I couldn’t find something to fully automate the SpamAssassin sa-learn process for virtual email users in a MySQL database, I wrote my own (borrowing liberally from Jason Schaefer’s SpamAssassin training and spam cleanup script).

This script assumes that:

  • You have a MySQL database with virtual users, with a user table called ‘virtual_users’ and the full email address stored in a field called ’email’.
  • Your email is stored in Maildir folders, with a heirarchy starting from /var/vmail/…

## Database details


## Where to log stuff


## How many days to wait before deleting spam
## Comment out to disable


echo -e "\n\nRun started `date +%c`"  >> $LOG 2>&1

## Spam and ham training for all virtual users
## Delete spam older than $CLEAN days

mysql --skip-column-names -u$USER -p$PASS -h$HOST -D$DB -e "SELECT SUBSTRING(email, 1, LOCATE('@', email) - 1) AS user, SUBSTRING(email, LOCATE('@', email) + 1) AS domain FROM virtual_users" | while read user domain;

  ## Spam
  echo "Spam training for $user@$domain" >> $LOG 2>&1
  /usr/bin/sa-learn --no-sync --spam /var/vmail/$domain/$user/.Junk/{cur,new} >> $LOG 2>&1
  ## Ham
  echo "Ham training for $user@$domain" >> $LOG 2>&1
  /usr/bin/sa-learn --no-sync --ham /var/vmail/$domain/$user/{cur} >> $LOG 2>&1
  ## Delete
  if [ -n $CLEAN ]; then
    echo "Deleting spam for $user@$domain older than $CLEAN days" >> $LOG 2>&1
    find /var/vmail/$domain/$user/.Junk/cur/ -type f -mtime +$CLEAN -exec rm {} \;

## Sync the SpamAssassin journal and print out stats

echo "Syncing the SpamAssassin journal" >> $LOG 2>&1
/usr/bin/sa-learn --sync >> $LOG 2>&1
echo "Statistics for this run:" >> $LOG 2>&1
/usr/bin/sa-learn --dump magic >> $LOG 2>&1

echo -e "Run finished `date +%c`"  >> $LOG 2>&1


Postfix with SPF, DKIM and DMARC

Using the tutorials at for setting up Postfix with virtual users and domains is generally excellent, but the tutorial for Debian Jessie (with Postfix 2.11) doesn’t contain instructions for setting up authenticated mail delivery (SPF, DKIM and DMARC). Here are my notes:


  • An otherwise-functioning email setup (these notes build on the tutorial)
  • Understanding of (and ability to modify) DNS records

Inbound SPF

SPF has its pros and cons, but on the whole it’s pretty useful. I’d recommend configuring inbound SPF checks to only reject mail that has a “hard fail” for SPF (vs. “soft fail” or “neutral”).

Install the following:

sudo aptitude install postfix-policyd-spf-python

In /etc/postfix/, in the section shown below – add the check_policy_service line before any RBL checks:

smtpd_recipient_restrictions =
  check_policy_service unix:private/policy-spf

Other SPF tutorials recommend extending the SPF time limit to 3600 seconds (from a default of 1000 seconds) – assuming your server is on a decently quick connection, I can’t see a good reason to change the default.

In /etc/postfix/, enable the SPF service as follows:

policy-spf unix -       n       n       -       -       spawn
  user=nobody argv=/usr/bin/policyd-spf

In /etc/postfix-policyd-spf-python/policyd-spf.conf, ensure the configuration is set to only reject messages that “hard fail” the SPF check – remove the existing HELO_reject line and replace as follows:

HELO_reject = Fail

Restart postfix:

sudo systemctl restart postfix

Send yourself a test email (from Gmail or similar) to ensure you’re getting lines like this in your mail.log:

policyd-spf[22747]: None; identity=helo; client-ip=2607:f8b0:4003:c06::230;;;
policyd-spf[22747]: Pass; identity=mailfrom; client-ip=2607:f8b0:4003:c06::230;;;

Outbound SPF

Configuring outbound SPF involves working out what mail servers you could potentially send through and then crafting an appropriate DNS TXT record (note that DNS SPF records have been deprecated and shouldn’t be used).

There are plenty of decent wizards and tutorials out there, but I like the easySPF wizard. Because I run my own mail server and never, ever send through any other mail service, my record looks like this: IN TXT "v=spf1 mx -all"

If I also wanted to use Gmail or Google Apps to send email from my domain, I’d need to change it to look like this: IN TXT "v=spf1 mx -all"

You can absolutely break mail delivery for your domain using SPF, so it’s really important to understand what you’re putting in to your DNS records.


DKIM is much, much easier than it used to be – and is becoming more and more important in the fight against spam. For a more comprehensive introduction, Eric Allman from Sendmail put together a nice overview of DKIM.

Different tutorials use different packages – I install the following:

sudo aptitude install opendkim opendkim-tools libmail-dkim-perl libcrypt-openssl-random-perl libcrypt-openssl-rsa-perl

The first two packages install OpenDKIM; the remaining packages are so SpamAssassin can handle DKIM signed messages.

Make sure the following directories exist (create them if they don’t):

sudo mkdir /etc/opendkim
sudo mkdir /etc/opendkim/keys

My /etc/opendkim.conf file is as follows. You’ll note that it differs from other tutorials in two main ways:

  • I’m using virtual domains, so I’m not putting everything in the main configuration file
  • I’m not using PID files to communicate with Postfix, so I don’t have things like UMask and PidFile set
AutoRestart             yes
AutoRestartRate         10/1h

Syslog                  yes
SyslogSuccess           yes
LogWhy                  yes

Canonicalization        relaxed/simple

ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts
KeyTable                refile:/etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable

Mode                    sv
SignatureAlgorithm      rsa-sha256

UserID                  opendkim:opendkim

Socket                  inet:12345@localhost

OversignHeaders         From

Note the Socket line above – that needs to be configured elsewhere as well.

In /etc/default/opendkim, comment out everything except the following:

SOCKET="inet:12345@localhost" # listen on loopback on port 12345

In /etc/postfix/

milter_protocol = 6
milter_default_action = accept
smtpd_milters = inet:localhost:12345
non_smtpd_milters = $smtpd_milters

If you’ve followed the tutorial, you will already have smtpd_milters = unix:/spamass/spamass.sock – if so, the smtpd_milters line above should be changed to:

smtpd_milters = unix:/spamass/spamass.sock, inet:localhost:12345

Create the TrustedHosts, KeyTable and SigningTable files as follows (substitute your domain(s) for in all of them)



Substitute the second block of IP addresses with real addresses from your server – don’t leave the Google DNS addresses there! Note that the /64 at the end of the IPV6 address is required.


Note multiple domain substitutions; DKIM also uses ‘selectors’ which are defined here. I’ve used “mail” as the selector (some folk will recommend something like “jan2016”), but you can use whatever you’d like:

/etc/opendkim/SigningTable (again with the multiple domain substitutions):


You’ll notice that the KeyTable file mentions a directory for your domain – create that now:

sudo mkdir /etc/opendkim/keys/

In the directory you just created, create the private/public key pair (note another domain substitution, and if you changed the selector above from “mail” to something else, you’ll need to change it here too):

sudo opendkim-genkey -b 2048 -s mail -d

Change the owner of the private key:

sudo chown opendkim:opendkim mail.private

The mail.txt file contains the TXT record you need to add to your DNS. Note that the key is broken up in to multiple quoted sections – be careful when adding it to your DNS records to make sure it’s all added correctly.

Wait until the DNS change has propagated, then confirm that the DKIM key in your DNS records passes a validation check. If everything looks good, restart the opendkim and postfix processes:

sudo systemctl restart opendkim
sudo systemctl restart postfix

Send a test email to a Gmail account. Here’s what you should see in the mail logs for the outbound message:

opendkim[11862]: D11F114939: DKIM-Signature field added (s=mail,

In Gmail, when the test email arrives, it should have the following headers:

       spf=pass ( domain of designates [IP Address] as permitted sender);
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple;; s=mail;

Reply back to the email and check the mail logs for the following:

opendkim[11862]: 0EBC014939: [2607:f8b0:4003:c01::235] not internal
opendkim[11862]: 0EBC014939: not authenticated
opendkim[11862]: 0EBC014939: DKIM verification successful
opendkim[11862]: 0EBC014939: s=20120113 SSL

If you have SpamAssassin installed, you should also see something like the following:

spamd[18803]: spamd: result: . 0 - DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FROM,HTML_MESSAGE,SPF_PASS,UNPARSEABLE_RELAY scantime=0.4,size=2698,user=user,uid=5000,required_score=5.0,rhost=::1,raddr=::1,rport=52616,mid=<>,autolearn=ham autolearn_force=no

Another test you can (and should) run is the Newsletters spam test, as it will make sure your mail is correctly signed using DKIM.

Once you are absolutely certain that both SPF and DKIM are working correctly…

Outbound DMARC

Similar to outbound SPF, outbound DMARC is set up as a DNS TXT record that defines how receiving mail servers should handle messages that fail SPF and DKIM checks. DMARC is only useful if SPF and DKIM are working for your domain, AND if all outbound mail for your domain goes through your servers.

If other servers are used (e.g., using an ISP mail server as a smarthost), then do not configure a DMARC record!

I really like Google’s guide for Preventing outgoing spam with DMARC and consider it mandatory reading.

Google’s guide will help you configure your own DMARC record, but as an example – here’s mine: IN TXT "v=DMARC1; p=quarantine; pct=100;"

Inbound DMARC

Install the following:

sudo aptitude install opendmarc

My /etc/opendmarc.conf – I’ve chosen to not send forensic/aggregate reports and to not reject mail that fails DMARC (for the time being):

AutoRestart             Yes
AutoRestartRate         10/1h

UserID                  opendmarc:opendmarc
Socket                  inet:54321@localhost
Syslog                  true
SyslogFacility          mail

AuthservID              mail.server.fqdn
#TrustedAuthservIDs      other.mail.server, another.mail.server
IgnoreHosts             /etc/opendkim/TrustedHosts

RejectFailures          false

Uncomment the TrustedAuthservIDs if you have additional mail server(s) configured for the same domain(s) that also have OpenDMARC installed.

Next, configure the socket by adding the following line to /etc/default/opendmarc (don’t uncomment anything):


Add to the list of milters in /etc/postfix/

smtpd_milters = unix:/spamass/spamass.sock, inet:localhost:12345, inet:localhost:54321

Start and restart the appropriate processes:

sudo systemctl start opendmarc
sudo systemctl restart postfix

Send yet another test email from Gmail and then look for the following in the mail logs:

opendmarc[23774]: E24C514939: pass

You’ll also see the following in the mail header:

Authentication-Results: mail.server.fqdn; dmarc=pass