Back in May, I wrote about how Let's Encrypt and Cloudflare DNS Validation could be used to setup auto-renewing SSL certificates for the CloudKey. The original blog post was written for v1.x firmware – which was the current version at the time.

CloudKey running 2.x firmware (a.ka. UniFi OS)

Recently, Ubiquiti has released version 2.x of their firmware for the CloudKey which is aimed at being a new, shared platform for all UniFi Controllers. This newer version of the software is called UniFi OS. The original process is now a little outdated, and thanks to Peter who commented on the original blog post, there is now a revised method.

The rest of this blog post is a re-write of the original post with the suggestions that Peter contributed incorporated into the blog post. This post is intended for anyone attempting this for the first time, and for anyone who may be upgrading from V1.

Tooling and Certificate Issuance

My Cloud Key sits on my network and isn't directly exposed to the internet, and I plan to keep it this way. I also haven't paid for a SSL Certificate in probably around 5 years thanks to things like Let's Encrypt and other service providers issues certificates for free, and I plan to use a free certificate for my Cloud Key too.

Whenever a Certificate Authority issues a certificate they have to ensure that you are in control of the domain that you want the certificate for. Projects like Let's Encrypt do this using an ACME (Automatic Certificate Management Environment) challenge to verify that you own the domain.

There are a number of challenge types and the Let's Encrypt documentation does a good job of describing the challenge types and their pros and cons. A HTTP Challenge is possibly the most popular challenge where you end up hosting a file (e.g. http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>) on your server to prove that you're in control of the domain.

A HTTP challenge works well when you're server is exposed to the internet. In the case of my Cloud Key, I own the domain that I want to use, but I don't have it exposed to the internet, nor do I want to change that. This is where a DNS Challenge comes in useful.

With a DNS challenge I can upload a DNS record (e.g. _acme-challenge.<YOUR_DOMAIN>) that will prove I have control of the domain. This is ideal for my use case as I can create the DNS record without exposing my Cloud Key to the internet.

Installing Let's Encrypt

Start by SSHing onto your Cloud Key and installing Let's Encrypt CLI tooling by running:

curl https://get.acme.sh | sh

Note: If you already have the ACME client installed because you followed the V1 version of this article you will want to make sure you have the latest version of the ACME client, the simplest way to do this is to re-install:

rm -rf ~/.acme.sh/
curl https://get.acme.sh | sh

This will look a little like this:

$ ssh ubnt@192.168.1.2

Linux UniFi-CloudKey-Gen2-Plus 3.18.44-ubnt-qcom #1 SMP Tue Dec 15 17:12:29 CST 2020 aarch64

Firmware version: v2.0.24
                .--.__
  ______ __ .--(    ) )-.   __ __                    __
 |      |  (._____.__.___)_|  |  |__ _____ __ __   _|  |_
 |   ---|  ||  _  |  |  |  _  |    <|  -__|  |  | |_    _|
 |______|__||_____|_____|_____|__|__|_____|___  |   |__|
        (c) 2020 Ubiquiti Inc.            |_____|

      Welcome to the CloudKey Plus!
Last login: Wed Dec 30 20:35:50 2020 from 192.168.1.46

root@UniFi-CloudKey-Gen2-Plus:~# rm -rf ~/.acme.sh/
root@UniFi-CloudKey-Gen2-Plus:~# curl https://get.acme.sh | sh
[Wed Dec 30 20:49:52 GMT 2020] Installing from online archive.
[Wed Dec 30 20:49:52 GMT 2020] Downloading https://github.com/acmesh-official/acme.sh/archive/master.tar.gz
[Wed Dec 30 20:49:52 GMT 2020] Extracting master.tar.gz
[Wed Dec 30 20:49:52 GMT 2020] It is recommended to install socat first.
[Wed Dec 30 20:49:52 GMT 2020] We use socat for standalone server if you use standalone mode.
[Wed Dec 30 20:49:52 GMT 2020] If you don't use standalone mode, just ignore this warning.
[Wed Dec 30 20:49:52 GMT 2020] Installing to /root/.acme.sh
[Wed Dec 30 20:49:52 GMT 2020] Installed to /root/.acme.sh/acme.sh 
[Wed Dec 30 20:49:53 GMT 2020] Installing alias to '/root/.bashrc' 
[Wed Dec 30 20:49:53 GMT 2020] OK, Close and reopen your terminal to start using acme.sh
[Wed Dec 30 20:49:53 GMT 2020] Installing cron job
no crontab for root
no crontab for root
[Wed Dec 30 20:49:53 GMT 2020] Good, bash is found, so change the shebang to use bash as preferred.
[Wed Dec 30 20:49:54 GMT 2020] OK
[Wed Dec 30 20:49:54 GMT 2020] Install success!

I'm planning on using a DNS Challenge so that Let's Encrypt can verify that I control the domain, and continue to that moving forward as the certificate needs renewing. The DNS for my domain is managed via Cloudflare which is supported by Let's Encrypt. The ACME DNS API will need an API token in order to update DNS settings.

Create a token via the Cloudflare Dashboard:

Use the Edit zone DNS template:

I chose to restrict the Zones that the API token can access by explicitly including the relevant zone under Zone Resources:

Go back to the CloudKey SSH session and modify the ~/.bashrc file to set the following environment variables to the relevant values for your API token and zone:

export CF_Token="xxxxx"
export CF_Account_ID="xxxxx"
export CF_Zone_ID="xxxxx"

I put all three of these environment variables before the line

. "/root/.acme.sh/acme.sh.env"

Once you've done this restart your SSH session so that the environment variables are picked up, and to pick up the bash alias that the acme installation created.

We can now issue our certificate I've set my -d argument to unifi.home.jamesridgway.co.uk, you will want to change this for the hostname that you want to use:

acme.sh --force --issue --dns dns_cf -d unifi.home.jamesridgway.co.uk --pre-hook "tar -zcvf /root/.acme.sh/CloudKeySSL_`date +%Y-%m-%d_%H.%M.%S`.tgz /data/unifi-core/config/unifi-core.*" --fullchainpath /data/unifi-core/config/unifi-core.crt --keypath /data/unifi-core/config/unifi-core.key --reloadcmd "systemctl restart unifi-core.service"

This should result in an output that looks a like this:

root@UniFi-CloudKey-Gen2-Plus:~# acme.sh --force --issue --dns dns_cf -d unifi.home.jamesridgway.co.uk --pre-hook "tar -zcvf /root/.acme.sh/CloudKeySSL_`date +%Y-%m-%d_%H.%M.%S`.tgz /data/unifi-core/config/unifi-core.*" --fullchainpath /data/unifi-core/config/unifi
-core.crt --keypath /data/unifi-core/config/unifi-core.key --reloadcmd "systemctl restart unifi-core.service"
[Wed Dec 30 20:51:36 GMT 2020] Using CA: https://acme-v02.api.letsencrypt.org/directory
[Wed Dec 30 20:51:36 GMT 2020] Run pre hook:'tar -zcvf /root/.acme.sh/CloudKeySSL_2020-12-30_20.51.35.tgz /data/unifi-core/config/unifi-core.*'
tar: Removing leading `/' from member names
/data/unifi-core/config/unifi-core.crt
/data/unifi-core/config/unifi-core.key
[Wed Dec 30 20:51:37 GMT 2020] Create account key ok.
[Wed Dec 30 20:51:37 GMT 2020] Registering account: https://acme-v02.api.letsencrypt.org/directory
[Wed Dec 30 20:51:39 GMT 2020] Registered
[Wed Dec 30 20:51:39 GMT 2020] ACCOUNT_THUMBPRINT='redacted'
[Wed Dec 30 20:51:39 GMT 2020] Creating domain key
[Wed Dec 30 20:51:39 GMT 2020] The domain key is here: /root/.acme.sh/unifi.home.jamesridgway.co.uk/unifi.home.jamesridgway.co.uk.key
[Wed Dec 30 20:51:39 GMT 2020] Single domain='unifi.home.jamesridgway.co.uk'
[Wed Dec 30 20:51:39 GMT 2020] Getting domain auth token for each domain
[Wed Dec 30 20:51:41 GMT 2020] Getting webroot for domain='unifi.home.jamesridgway.co.uk'
[Wed Dec 30 20:51:41 GMT 2020] Adding txt value: 7k-redacted for domain:  _acme-challenge.unifi.home.jamesridgway.co.uk
[Wed Dec 30 20:51:44 GMT 2020] Adding record
[Wed Dec 30 20:51:45 GMT 2020] Added, OK
[Wed Dec 30 20:51:45 GMT 2020] The txt record is added: Success.
[Wed Dec 30 20:51:45 GMT 2020] Let's check each DNS record now. Sleep 20 seconds first.
[Wed Dec 30 20:52:06 GMT 2020] Checking unifi.home.jamesridgway.co.uk for _acme-challenge.unifi.home.jamesridgway.co.uk
[Wed Dec 30 20:52:07 GMT 2020] Domain unifi.home.jamesridgway.co.uk '_acme-challenge.unifi.home.jamesridgway.co.uk' success.
[Wed Dec 30 20:52:07 GMT 2020] All success, let's return
[Wed Dec 30 20:52:07 GMT 2020] Verifying: unifi.home.jamesridgway.co.uk
[Wed Dec 30 20:52:11 GMT 2020] Success
[Wed Dec 30 20:52:11 GMT 2020] Removing DNS records.
[Wed Dec 30 20:52:11 GMT 2020] Removing txt: 7k-redacted for domain: _acme-challenge.unifi.home.jamesridgway.co.uk
[Wed Dec 30 20:52:15 GMT 2020] Removed: Success
[Wed Dec 30 20:52:15 GMT 2020] Verify finished, start to sign.
[Wed Dec 30 20:52:15 GMT 2020] Lets finalize the order.
[Wed Dec 30 20:52:15 GMT 2020] Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/...'
[Wed Dec 30 20:52:16 GMT 2020] Downloading cert.
[Wed Dec 30 20:52:16 GMT 2020] Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/...'
[Wed Dec 30 20:52:17 GMT 2020] Cert success.
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
[Wed Dec 30 20:52:17 GMT 2020] Your cert is in  /root/.acme.sh/unifi.home.jamesridgway.co.uk/unifi.home.jamesridgway.co.uk.cer
[Wed Dec 30 20:52:17 GMT 2020] Your cert key is in  /root/.acme.sh/unifi.home.jamesridgway.co.uk/unifi.home.jamesridgway.co.uk.key
[Wed Dec 30 20:52:17 GMT 2020] The intermediate CA cert is in  /root/.acme.sh/unifi.home.jamesridgway.co.uk/ca.cer
[Wed Dec 30 20:52:17 GMT 2020] And the full chain certs is there:  /root/.acme.sh/unifi.home.jamesridgway.co.uk/fullchain.cer
[Wed Dec 30 20:52:17 GMT 2020] Installing key to:/data/unifi-core/config/unifi-core.key
[Wed Dec 30 20:52:17 GMT 2020] Installing full chain to:/data/unifi-core/config/unifi-core.crt
[Wed Dec 30 20:52:17 GMT 2020] Run reload cmd: systemctl restart unifi-core.service
[Wed Dec 30 20:52:30 GMT 2020] Reload success

As part of the process, the ACME client will install the certificates into the correct locations and restart the unfi-core.service so that the certificate change is applied immediately.

Finally, add the following entry to your crontab file to ensure that the certificate is renewed automatically:

0 0 * * * /root/.acme.sh/acme.sh --renew -d unifi.home.jamesridgway.co.uk --pre-hook "tar -zcvf /root/.acme.sh/CloudKeySSL_`date +%Y-%m-%d_%H.%M.%S`.tgz /data/unifi-core/config/unifi-core.*" --fullchainpath /data/unifi-core/config/unifi-core.crt --keypath /data/unifi-core/config/unifi-core.key --reloadcmd "systemctl restart unifi-core.service"

Again, just make sure to chagne the -d argument to your domain.

Done!

That's it, we're done!

The entire process for newer UniFi OS (V2 version of the firmware) is much simpler than the original approach. Thanks again to Peter for pointing out the alterations to the original method.