abond.dev

Unifi Network Server Certificates

network graph showing the number of downloads and uploads
author Anthony in a plaid shirt, smiling for the camera
Anthony Bond

Ubiquiti Network hardware can be known as a prosumer network system for folks looking to dive into the networking space. What started as a single access point and security gateway, has evolved into a small office network that handles realtime stream of camera feeds and providing in-depth network analytics.

Overtime I've also found myself burning countless hours tweaking the network, vlans, firewalls, and causing network disruption to my family's access to the internet. Of course, I never filed a change request with the change board and that has resulted in a code freeze for 364 days out of the year.

Ubiquiti also develops a suite of software tools to streamline management of their devices and provide users with enterprise grade networking features. One of those tools is the UniFi Network Server. While Ubiquiti provides cloud hosting and integrated Cloud Gateways that include the UniFi Network Server. Users can also run the service on their own hardware. I've been using an old MacPro running Ubuntu with Docker to host my network server. Now that I've stopped playing with all the network configurations, management is pretty straightforward. I update the container image to a new release version and restart the service. UniFi automatically runs backups once a day and those are saved to the host filesystem. Pretty sweet setup that is no fuss and online 99% of the time.

One challenge with this entire setup is the self-signed certificate that UniFi presents to the web server. Browsers do not like it when you hit a self-signed certificate, nor do other applications that interact with the UniFi network server. For me, this drove me crazy always hitting the untrusted certificate error. I had to do something about it!

I went to the web in search of solution and what do you know the UniFi Network Server provides a `import_key_cert` command. Problem is this would sometimes work and sometimes throw and obscure error. The community forms is riddled with comments and workarounds, but nothing worked for what I wanted. Remember how I said Ubiquiti is known as prosumer product. Well most of those community members didn't have a problem spending money for a certificate to be issued through DigiCert or Namecheap. Me, I like the free community route of Let's Encrypt.

So, how do I go about getting a Let's Encrypt signed certificate into the UniFi Network Server?

For a while cert-manager was the tool of choice generating certificates every 60 days and uploading to my container. While this was great, I eventually lost my Raspberry Pi Kubernetes cluster to a series of hardware failures and frustration of playing with Kubernetes.

I finally sat down over the weekend (two years later) and revisited my setup. I'm happy to say I've solved my problem with a minimal configuration using Lego, bash, and systemd timers. Below is the path I took to solve my certificate challenges, hopefully it can help you solve your certificate problems too.

Prerequisites:

  • DNS service provider (Cloudflare)
  • Domain name
  • Lego install
  • Linux machine

Step 1: Lego Hook

The lego CLI allows users to invoke hooks. These hooks can be programs or bash scripts. I use the hook to copy the certificates to a directory mounted by the UniFi container. After copying the certificates the script executes a docker command to signal UniFi to load the new certificates and restart the container.

Feel free to copy the contents below to `/root/legohook.sh` or another location to fit your needs. Make you update the `CERT_NAME` to your domain name and the docker command if you're not using a docker container.

```

#!/usr/bin/env bash
#
# Load the Cloudflare token in your environment
# CLOUDFLARE_DNS_API_TOKEN="somevalue"
#
# Create certificate
# lego -d unifi.example.com --email="[email protected]" --dns="cloudflare" --dns.resolvers 8.8.8.8:53 run --run-hook="/root/legohook.sh"
#
# Renew certificate
# lego --email="[email protected]" --domains="unifi.example.com" --dns="cloudflare" --dns.resolvers 8.8.8.8:53 renew --renew-hook="/root/legohook.sh"

# Hook provided env vars
# LEGO_ACCOUNT_EMAIL: the email of the account.
# LEGO_CERT_DOMAIN: the main domain of the certificate.
# LEGO_CERT_PATH: the path of the certificate.
# LEGO_CERT_KEY_PATH: the path of the certificate key.
set -xe
CERT_NAME=unifi.example.com

LEGO_PATH=/root/.lego
CERTS_PATH=$LEGO_PATH/certificates

UNIFI_DIR=/data/unifi

CONTAINER_ID=$(docker ps -f name=unifi -q)

# Copy certificates to Unifi Data directory on the host
cp ${CERTS_PATH}/${CERT_NAME}.key ${UNIFI_DIR}/${CERT_NAME}.key
cp ${CERTS_PATH}/${CERT_NAME}.crt ${UNIFI_DIR}/${CERT_NAME}.crt
cp ${CERTS_PATH}/${CERT_NAME}.issuer.crt ${UNIFI_DIR}/${CERT_NAME}.issuer.crt

# Unifi import certificates
docker exec $CONTAINER_ID java -jar /usr/lib/unifi/lib/ace.jar import_key_cert ${CERT_NAME}.key ${CERT_NAME}.crt ${CERT_NAME}.issuer.crt

# Restart Unifi
systemctl restart unifi

```

Step 2: Create the first certificate

Use the following lego command to create the initial certificate. Replace the parameters to meet your requirements.

```

lego -d unifi.example.com \
--email="[email protected]" \
--dns="cloudflare" \
--dns.resolvers 8.8.8.8:53 \
run \
--run-hook="/root/legohook.sh"

```

Note, you can also use the Let's Encrypt staging api to test the entire process. Add the following parameter before the `run` flag to receive certificates signed by the staging Let's Encrypt server.

```

--server=https://acme-staging-v02.api.letsencrypt.org/directory

```

Step 3: Configure the timer

Setup a systemd timer to execute once a day to invoke the lego renew command. Don't worry lego will exit quickly when the certificate is not ready for renewal.

Timer

Create a timer file under `/etc/systemd/system/lego.timer` with the following contents.

```

[Unit]
Description="Run lego.service 45 minutes after boot and every 24 hours relative to activation time"

[Timer]
OnBootSec=45min
OnUnitActiveSec=24h
OnCalendar=Mon..Sun *-*-* 10:01:00
Unit=lego.service

[Install]
WantedBy=multi-user.target

```

Unit

Create a systemd unit that will be invoked by the timer above. Place the following contents in `/etc/systemd/system/lego.service` and update to meet your environment needs. You can see the unit invokes the lego command for renew and calls the `legohook.sh` script we created in the first step. I also pull in environment variables from the `/root/.cloudflare` file to allow lego access to Cloudflare.

```
[Unit]
Description="Unifi certificate renewal with lego"

[Service]
Type=oneshot
WorkingDirectory=/root
EnvironmentFile=/root/.cloudflare
ExecStart=/usr/local/bin/lego --email="[email protected]" --domains="unifi.example.com" --dns="cloudflare" --dns.resolvers 8.8.8.8:53 renew --renew-hook="/root/legohook.sh"

```

Step 4: Reload Systemd

Reload systemd and test your timer.

```

systemctl daemon-reload && systemctl start lego.timer

```

Validate the command ran successfully, if so you should have a new certificate loaded into your UniFi Network server and logs within the journal stating your certificate is not ready for renewal.

Example successful log lines: `journalctl -u lego.service`

```

Aug 18 20:38:55 host01 systemd[1]: Started "Unifi certificate renewal with lego".
Aug 18 20:38:56 host01 lego[209811]: 2024/08/18 20:38:56 [unifi.example.com] The certificate expires in 89 days, the number of days defined to perform the renewal is 30: no renewal.
Aug 18 20:38:56 host01 systemd[1]: lego.service: Deactivated successfully.

```

Don't forget to enable the timer, so systemd can execute the command daily.

```

systemctl enable lego.timer

```

Feel free to reach out, if you run into any challenges or have questions. Thanks for making it this far. :)