UniFi Controller 5.11, Let’s Encrypt SSL and Docker

A slight change of plans from earlier posts on the topic of UniFi Controllers! Here’s how to get a UniFi Controller running inside a Docker container, along with a trusted Let’s Encrypt SSL certificate.

Note: this guide assumes you’re configuring things on a server or VM with public Internet access. You’ll also need a fixed public IP and functional DNS to get an SSL certificate.

Here we go:

Firewall

UniFi needs a bunch of inbound ports open. Here’s the official list – it differs slightly to what I use:

PortDescription
UDP/3478STUN – required for device communication with the controller
TCP/8080Inform – required to adopt devices
TCP/8443GUI – required even if you use the Cloud Controller access
TCP/8880Captive Portal – HTTP – only needed if you use the captive portal feature
TCP/8843Captive Portal – HTTPS – only needed if you use the captive portal feature
TCP/6789Speed Test – only needed if you use the speed test feature

Let’s Encrypt also needs a port open:

PortDescription
TCP/80HTTP – required for the HTTP-01 challenge type

I use ufw to configure iptables – first, set up an application definition for the UniFi Controller – in /etc/ufw/applications.d/unifi:

[unifi]
title=unifi
description=UniFi Controller
ports=6789,8080,8880,8443,8843/tcp|3478/udp

Run the following four commands to configure and enable the firewall. I’ve made some assumptions about what’s needed – you may need to customise things a little more:

sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow unifi
sudo ufw enable

User Account

UniFi probably shouldn’t be run as root – this is generally a good idea, plus it may also become a requirement for the Docker image I’m using in the future. This will also affect what ports you can configure the controller to use – the default ports work fine for any user, but changing any of the ports to <1024 requires root.

Create the unifi user and group accounts:

sudo adduser unifi --system --group --no-create-home

Pay attention to the UID and GID that get created; you need them in the Docker Compose file below.

Docker

Here’s the tl;dr version of the installation instructions, but if you want to read the full version with all the details – check the Docker website.

Configure the Docker repository – it contains a more up-to-date version:

sudo apt-get update && sudo apt-get upgrade
sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

Install Docker and related tools:

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

UniFi Controller

There are a number of UniFi Docker images out there, but I like the one by jacobalberty as it’s kept up to date – plus it exposes a volume for adding trusted certificates. His Docker Compose file isn’t quite to my taste, so I’ve adjusted things. Create the file /opt/unifi/docker-compose.yml:

version: '2.2'
services:
  mongo:
    image: 'mongo:3.4'
    restart: always
    volumes:
      - db:/data/db
  controller:
    image: 'jacobalberty/unifi:${TAG:-latest}'
    depends_on:
      - mongo
    init: true
    restart: always
    volumes:
      - data:/unifi/data
      - log:/unifi/log
      - cert:/unifi/cert
      - init:/unifi/init.d
    environment:
      RUNAS_UID0: 'false'
      UNIFI_UID: 100
      UNIFI_GID: 100
      JVM_MAX_THREAD_STACK_SIZE: 1280k
      DB_URI: mongodb://mongo/unifi
      STATDB_URI: mongodb://mongo/unifi_stat
      DB_NAME: unifi
    ports:
      - '3478:3478/udp'
      - '6789:6789/tcp'
      - '8080:8080/tcp'
      - '8443:8443/tcp'
      - '8880:8880/tcp'
      - '8843:8843/tcp'
  logs:
    image: bash
    depends_on:
      - controller
    command: bash -c 'tail -F /unifi/log/*.log'
    restart: always
    volumes:
      - log:/unifi/log

volumes:
  db:
  data:
  log:
  cert:
  init:

Note: if you’re going to change the location of this file, it should be in a directory called ‘unifi’. Bring the stack up like so (it will take a fair while first time around):

sudo docker-compose up -d

Install SSL

This part requires a few sections that need to be completed in order – first you need a script to load the SSL certificate into the UniFi Docker cert volume, then you need to run a certbot command to obtain the certificate.

If you use a provider other than Let’s Encrypt for SSL certificates, these instructions will need to be adjusted.

UniFi SSL Deploy Script

It may seem backwards, but the deploy script needs to exist before obtaining the certificate. Read through this script carefully and adjust any domains and directories as needed. Create the file /opt/unifi/unifi-ssl-deploy.sh:

#!/bin/sh

set -e

for domain in $RENEWED_DOMAINS; do
  case $domain in
  unifi.example.com)
    # Where does the Docker cert data volume live?
    cert_root=/var/lib/docker/volumes/unifi_cert/_data
    # Where is the Docker Compose file?
    compose_file=/opt/unifi/docker-compose.yml

    # Make sure the certificate and private key files are
    # never world readable, even just for an instant while
    # we're copying them into cert_root.
    umask 077

    cp "$RENEWED_LINEAGE/cert.pem" "$cert_root/cert.pem"
    cp "$RENEWED_LINEAGE/privkey.pem" "$cert_root/privkey.pem"
    cp "$RENEWED_LINEAGE/chain.pem" "$cert_root/chain.pem"

    # Apply the proper file permissions
    # Files can be owned by root
    chmod 400 "$cert_root/cert.pem" \
      "$cert_root/privkey.pem" \
      "$cert_root/chain.pem"

    # Restart the Docker container
    docker-compose -p unifi -f $compose_file stop
    docker-compose -p unifi -f $compose_file start
    ;;
  esac
done

Now make the file executable:

sudo chmod a+x unifi-ssl-deploy.sh

Obtain SSL with Certbot

Conveniently, Certbot has its own mechanism for obtaining an SSL certificate without using a webserver. If you have a webserver configured, you will want to adjust these instructions accordingly.

As above, adjust the following to suit your domain:

sudo apt-get install certbot
sudo certbot certonly --standalone --domain unifi.example.com --deploy-hook /opt/unifi/unifi-ssl-deploy.sh

The command to obtain the certificate will ask a few questions – you may also see an error from the deploy script, but it’s not actually an error per se.

Note: After the deploy script has run, you need to wait up to 5 minutes for the UniFi Controller to fully start back up again. If you don’t, you’re likely to get an SSL error (PR_END_OF_FILE_ERROR) in the browser!

We’re all done – your UniFi Controller should now be available via: https://unifi.example.com:8443

Reverse Proxy

I’ve opted to not configure a reverse proxy, as I don’t believe one is needed. If port 8443 is blocked on your network, you can configure cloud access via https://unifi.ui.com.

If you want to configure a reverse proxy, note you’ll need something that handles websockets gracefully – Nginx and Traefik are probably your best options.

Easy Cookie Consent with Google Tag Manager

Google Tag Manager (GTM) should have a built-in method to manage firing tags based on user consent, but it doesn’t. While looking for a good way to set this up, I was bewildered by the insanely complex methods people were coming up with.. Thankfully, there’s an easier way that doesn’t involve editing site code!

Note: The below may help make your site more GDPR or CCPA compliant, but it’s not everything you need to do. Go talk to a lawyer and get some proper advice!

Google Tag Manager basics

First, set up Google Tag Manager on your website and use it to fire something like a Google Analytics tag. Doing so is fairly straight forward, and there are heaps of guides online to show you how it’s done. For WordPress sites, I can recommend the Google Tag Manager for WordPress plugin. You’ll need to have GTM firing at least one tag so you can see the difference between having and not having user consent.

Osano Cookie Consent basics

Next – go to Cookie Consent by Osano. Osano offer two versions of their consent tool: a paid version and an open source free version. Unless you feel like paying (and it’s not a bad service from all reports), you want the open source version.

The download page has a table comparing the two versions – at the bottom, click the Start Coding button. In the Configure section, go through steps 1-6 to set up how you want your consent prompt to appear.

Here’s where we start ignoring most of their instructions 😉

All Consent Types

Irrespective of which consent compliance option you selected, you need to load the Osano code as a tag. In Google Tag Manager, create a new Custom HTML tag:

  1. Give it a name: “Cookie Consent by Osano”
  2. Copy both blocks of code from the Osano site (Copy HTML followed by Copy Code) into the HTML box in GTM
  3. Configure a firing trigger so this tag loads on All Pages as a Page View

Save and publish in GTM. What you do next depends on what type of consent you need..

  1. Just let the user know we’re using cookies
  2. Give the user the ability to opt out of cookies
  3. Require the user to opt in to cookies before using them

Consent Type 1: Inform User

If you selected the compliance option “Just tell users that we use cookies” – there’s nothing more you need to do! The consent prompt should now load on each page until the user dismisses it. 🙂


Consent Type 2: Opt Out

If you selected the compliance option “Let users opt out of cookies (Advanced)” – there’s a little more to do, but it’s easy. Ignore the warning about needing to modify your site. 😉

The Cookie Consent script stores the user choice in a cookie, so we need to tell GTM to read it and use it:

Read Cookie

In GTM, create a new Variable:

  1. Call it “Cookie Consent Status” or similar
  2. Select ‘1st-Party Cookie’ as the variable type
  3. For Cookie Name, enter cookieconsent_status

Use Consent Status

Still in GTM, create a new Trigger:

  1. Call it “Cookie Consent equals deny”
  2. Select ‘Page View’ as the trigger type
  3. Choose ‘Some Page Views’
  4. Select ‘Cookie Consent Status’ (or whatever you called the variable) in the first drop down
  5. Choose ‘equals’ in the second drop down
  6. Enter deny as the text

The line should now read “Cookie Consent Status equals deny”.

To make use of this trigger, go into each of the tags that are configured to fire using a trigger (except the Cookie Consent tag, obviously). Add an “exception” trigger, then choose the ‘Cookie Consent equals deny’ trigger you created above.

Finally, save and publish in GTM – tags should now be suppressed if your users choose to deny cookies. It’s important to note that tags will fire even if the consent prompt is ignored, so if you need to stop serving up delicious cookies until someone allows them..


Consent Type 3: Opt In

Aka – the GDPR option. This one sucks because you won’t get analytics data unless someone clicks ‘Allow cookies’, but if it’s what you need to do..

First, read through Consent Type 2 above. You need to create the same Variable in the Read Cookie section, but then things are a bit different:

Are Cookies Definitely Allowed?

In Google Tag Manager, create a new Trigger:

  1. Call it “Cookie Consent does not equal allow”
  2. Select ‘Page View’ as the trigger type
  3. Choose ‘Some Page Views’
  4. Select ‘Cookie Consent Status’ (or whatever you called the variable) in the first drop down
  5. Choose ‘does not equal’ in the second drop down
  6. Enter allow as the text

The line should now read “Cookie Consent Status does not equal allow”. This is a little confusing, but this is different to the previous trigger in that it will also block tags if the user hasn’t made a choice. Logic is fun!

As with Consent Type 2, go into each of the tags that are configured to fire using a trigger (except the Cookie Consent tag). Add an “exception” trigger, then choose the ‘Cookie Consent does not equal allow’ trigger you just created.

One last thing – and this is optional – but when someone accepts cookies, you probably want to reload the page to fire all the tags that were being suppressed until now. To do so, you need to create another trigger and a tag:

Listen for the Allow Cookies click

We need to make sure GTM is paying attention to CSS classes on elements users click:

  1. In GTM, go to ‘Variables’
  2. Click ‘Configure’ in the ‘Built-In Variables’ section
  3. Scroll through the list and ensure the ‘Click Classes’ option is checked.

Now, create a trigger:

  1. Call it something like “Allow cookies button click”
  2. Choose ‘Click – All Elements’ as the trigger type
  3. Select ‘Some Clicks’
  4. Choose ‘Click Classes’ in the first drop down
  5. Select ‘contains’ in the second drop down
  6. Enter cc-allow as the text

The line should now read “Click Classes contains cc-allow”.

Reload page when the user accepts

When the user clicks on the Allow cookies button, we need to do something with it..

Still in GTM, create another Custom HTML tag:

  1. Call it “Reload Page”
  2. In the HTML box, enter the following:
    <script>window.location.reload();</script>
  3. Select ‘Allow cookies button click’ as the trigger

At long last – save and publish in GTM. This configuration prevents tags from firing unless they’re allowed and reloads the page when the user allows them so you don’t lose the pageview.

Configure Homebridge as a Service on Debian

Homebridge is a fantastic mechanism for getting non-HomeKit-certified smart home tech talking to Apple. It’s nerd tech written for nerds, so it’s not the easiest thing in the world to get running. Here are some instructions to make things work on Debian and friends (including Raspberry Pi and Ubuntu):

Note that these instructions are going off old memory, so they may be slightly wrong – I’ll retest and fix anything that needs fixing soon!

First thing’s first – make sure everything is up to date:

sudo apt-get update
sudo apt-get upgrade

Install Node.js

Next, install Node.js from the NodeSource GitHub (don’t use the repositories of your distro, as they’re probably outdated). After installation, update to the latest release and install build tools:

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs

sudo npm cache clean -f
sudo npm install -g n
sudo n stable

sudo apt-get install -y build-essential

I like to reboot at this point, but that’s in no way essential.

Install Homebridge

The following installs Homebridge (note the --unsafe-perm flag) and the Homebridge Dummy Switches plugin. The plugin isn’t critical, but I seem to remember having difficulties getting things working without at least one plugin. Besides, the Dummy Switches plugin can be fairly handy for certain types of automation..:

sudo apt-get install libavahi-compat-libdnssd-dev
sudo npm install -g --unsafe-perm homebridge
sudo npm install -g homebridge-dummy

Next, create a very basic config at ~/.homebridge/config.json:

{
  "bridge": {
    "name": "Homebridge",
    "username": "CC:22:3D:E3:CE:30",
    "port": 51826,
    "pin": "031-45-154"
  },
  "description": "Homebridge Server",
  "platforms": [
    {}
  ],
  "accessories": [
    {
      "accessory": "DummySwitch",
      "name": "My First Switch"
    }
  ]
}

Run Homebridge directly at the command line to test:

homebridge

An ascii-art QR code will appear; you can use this code to add Homebridge to HomeKit using the Home app. Ignore warnings about the accessory not being certified.

Create systemd service for Homebridge

The instructions below assume you used the instructions above to install Node.js and Homebridge – paths may be different if you didn’t.

Pre-requisites

The following commands create the service user and directories, then moves the existing configuration into the appropriate locations for starting Homebridge as a service. Finally, set the relevant permissions..:

sudo useradd -M --system homebridge
sudo mkdir /var/lib/homebridge

sudo cp ~/.homebridge/config.json /var/lib/homebridge/
sudo cp -r ~/.homebridge/persist /var/lib/homebridge

sudo chmod -R 0777 /var/lib/homebridge

Systemd config files

Create /etc/default/homebridge:

# Defaults / Configuration options for homebridge
# The following settings tells homebridge where to find
# the config.json file and where to persist the data
# (i.e. pairing and others)
HOMEBRIDGE_OPTS=-U /var/lib/homebridge

# If you uncomment the following line, homebridge will log more
# You can display this via systemd's journalctl:
# journalctl -f -u homebridge
#DEBUG=*

Create /etc/systemd/system/homebridge.service:

[Unit]
Description=Node.js HomeKit Server
After=syslog.target network-online.target

[Service]
Type=simple
User=homebridge
EnvironmentFile=/etc/default/homebridge
ExecStart=/usr/local/bin/homebridge $HOMEBRIDGE_OPTS
Restart=on-failure
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target

Finally, enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable homebridge
sudo systemctl start homebridge

Links and Howtos

Here’s what I used to get myself up and running:
https://github.com/nfarina/homebridge
https://github.com/nfarina/homebridge/blob/master/config-sample.json
https://gist.github.com/johannrichard/0ad0de1feb6adb9eb61a/
https://timleland.com/setup-homebridge-to-start-on-bootup/