Ubiquiti UniFi switches.

Secure Your Hosted UniFi Controller

BrianSnelgrove - May 25, 2020
Posted Under: Networking
Cloud-hosted UniFi Controllers, or any UniFi controller exposed to the public Internet for that matter, should be as secure as possible. If you follow Crosstalk Solutions guide for hosted UniFi controllers you may want to follow these steps to further secure your installation.

 

Unless otherwise noted, all directions are for Debian based systems. Most steps will work for other distributions but some commands may need modifications.
Foreword:

These steps are based on configurations outlined by Chis Sherwood at Crosstalk Solutions. If you don't already have your UniFi Controller running in the cloud, he has some great steps for a baseline configuration.  As good as his steps are, there are a few more things that can be done to beef up the security a little. 

Before we get started...

If you are putting your UniFi Controller in the cloud and you do not have a full UniFi ecosystem, you may have trouble getting new devices to see the hosted controller. If they can't find the controller you can't adopt them - NOT GOOD! Luckily there are several solutions to this issue. The easiest to explain, without knowing more about your network at least, is to SSH into any new devices and point them to your controller. Look in your DHCP server to find the IP address of the device you just connected, ssh into the device, and issue this command:

ssh ubnt@x.x.x.x
password: ubnt

set-inform http://ip-of-controller:8080/inform

Other options, along with this one, are explained on the Ubiquiti support website.

How do I get around this? Simple, I set the domain name in my local DHCP server to the same thing as my UniFi Controller domain and I gave my controller a hostname of unifi. When a device on my local network does a DNS lookup for unifi it gets resolved to unifi.mynetwork.com and the DNS lookup points to my hosted UniFi controller. I am using pfsense as my router/firewall: Services -> DHCP -> Domain Name. You can do something similar under Services -> DNS Resolver -> Host Overrides-> Add New then enter unifi as the host and the IP address of your UniFi Controller. If you are using an EdgeRouter there is a DHCP option for your UniFi Controller. Detailed directions are on help.ubnt.com.

Close Down Those Ports!

Closed ports make happy servers. Whenever possible, you should close all ports that are not required for your system to run and only allow network traffic you trust. If you followed the Crosstalk guide, you installed LetsEncrypt to provide valid SSL certificates for your UniFi Controller. While that is a great idea, it potentially leaves your server open to attacks through port 80 and 443. Pre and post hooks can be used to update firewall rules whenever certbot runs to update your LetsEncrypt certificates - let make that happen!

Lets create two small (like 3 lines small) scripts to enable port 80 and 443 when certbot starts:

sudo nano /etc/letsenctrypt/renewal-hooks/pre/ufw_allow.shl

#!/bin/bash
ufw allow 80
ufw allow 443

Make a script to close the ports when certbot finishes:

sudo nano /etc/letsencrypt/renewal-hooks/post/ufw_deny.shl

#/bin/bash
ufw deny 80
ufw deny 443

Make both files executable:

sudo chmod +x /etc/letsencrypt/renewal-hooks/pre/ufw_allow.shl
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/ufw_deny.shl

The next time certbot runs, either manually or through the installed cron job, it will open ports 80 and 443 for the renewal verification then close the ports when the process has completed. 

Get cockpit to use the LetsEncrypt certificate

Much like Chris Sherwood recommending the LetsEncrypt SSL certificate for the UniFi Controller, the same can be done for cockpit with a few easy steps. First, make a script and add a few lines to it:

sudo nano /usr/local/bin/cockpit_import_ssl.shl

#!/bin/bash
FQDN=`cat /etc/hostname`;     # make sure to use back ticks!
cd /etc/cockpit/ws-certs.d;      # go to the certs directory
# copy the two certificate files into the certs directory - make sure the correct double quotes are used!
cat /etc/letsencrypt/live/"$FQDN"/cert.pem > "$FQDN".cert
cat /etc/letsencrypt/live/"$FQDN"/privkey.pem >> "$FQDN".cert
systemctl restart cockpit     # restart cockpit

Make the file executable:

sudo chmod +x /usr/local/bin/cockpit_import_ssl.shl

Before you run this file there will be an existing SSL certificate that you want to rename so it is not picked back up by cockpit automatically. In my case it was called 0-self-signed.cert and it just needs to be renamed:

mv /etc/cockpit/ws-certs.d/0-self-signed.cert /etc/cockpit/ws-certs.d/0-self-signed.cert.bak

Lastly we should add an initialization script to the cron.daily so our certs get updated automatically:

sudo nano /etc/cron.daily/cockpit_import_ssl

#!/bin/bash
/usr/local/bin/cockpit_import_ssl.shl

Make sure the file is owned by root and is made executable:

sudo chown root.root /etc/cron.daily/cockpit_import_ssl
sudo chmod +x /etc/cron.daily/cockpit_import_ssl

If you manually run the cron and check your cockpit installation (https://your.host.name:9090) you should see your valid LetsEncrypt SSL. This needs to be done as root:

sudo -i
/etc/cron.daily/cockpit_import_ssl

Dynamic IP Address

The guide Chris posted has you add firewall rules to only allow access to your IP address. While that is a great idea what do you do if you have a dynamic IP address from your ISP? Check out the guide for DDNS Using GoDaddy as a pre-requisite for these steps. 

All these steps will be done as root since root will be the user executing the cron job every hour for us. First create a script and put in the following commands. You will need to update HOSTNAME with the DNS name you used in the DDNS guide.

sudo -i
nano /usr/local/bin/UniFiFirewallRules.shl

#!/bin/bash
HOST="$(getent hosts HOSTNAME | awk '{ print $1 }')";
yes | ufw delete "$(ufw status numbered | grep 8443 | cut -c 2-3)";
yes | ufw delete "$(ufw status numbered | grep 9090 | cut -c 2-3)";
ufw allow from $HOST to any port 8443;
ufw allow from $HOST to any port 9090;

Make the script executable:

chmod +x /usr/local/bin/UniFiFirewallRules.shl

Update cron.hourly to execute this new script

nano /etc/cron.hourly/UniFiFirewallRules

#!/bin/bash
/usr/local/bin/UniFiFirewallRules.shl

Make the cron.hourly executable

chmod +x /etc/cron.hourly/UniFiFirewallRules

Run the script to test it

/etc/cron.hourly/UniFiFirewallRules

Verify you can still access your UniFi controller. Your firewall rules should now automagically update if/when your external IP address changes. If you need to access your UniFi Controller when you are out and about you should VPN back into your network to access it!

Services/items I used

I have used quite a few Ubiquiti UniFi products, and I am a massive fan of their network hardware/software. There are some Ubiquiti products that I have not had the greatest experience with, but I have been nothing but happy with the networking products I have used. Prices are very reasonable, and the networks I put together have been rock solid. They are closer to enterprise-grade hardware and do take a bit more knowledge/setup than standard consumer-grade equipment, but they are well worth the extra work!

  • UAP-AC-PRO - High-Performance Access Point
  • UAP-AC-LR-US - Long Range Access Point
  • US-8-150W - 8 Port Switch with Power Over Ethernet, a great way to get your feet wet in the UniFi ecosystem
  • US-16-150W - 16 Port Switch with Power Over Ethernet, sized to fit into a standard 19-inch network rack
  • US-48-500W - 48 Port Switch with Power Over Ethernet, the big daddy!

I have also used a few non-UniFi products from Ubiquiti and liked them.

  • EdgeRouter X - Great router/firewall for small networks and works great at a very affordable price. If you need advanced IPS/IDS, VPN, etc. you probably want something a bit more robust.
  • LBE-5AC-GEN2-US - Nice wireless bridge that can easily support 300Mbs sustained transfer. They have been through some pretty bad storms and had ZERO downtime. Setup with the UNMS smartphone app was quick and easy.

I decided to go with Vultr for VPS hosting. Fair prices and easy account setup. I have several VPS's running in Vultr, and they all seem to be performing very well. As a side note - Chris Sherwood has nicely detailed directions for getting the SSH keys working in Windows/Putty for remote SSH access.

I started with the UniFi Controller running as a VirtualBox VM on an old HP computer that I got second hand (refurbished). It ran fine - attesting to the low processing power that the UniFi Controller needs - but I added a second network to my controller. I thought it was time to get something a bit more reliable than a used computer that was going on ten years old.


Discussion - all postings are moderated

Brian Snelgrove - November 25, 2020
Thank you for the comments Chris, I am glad this post helped you! #1 - FQDN is a little misleading, the variable is only used to name the certificate files and does not really need to be the fully qualified domain name. #2 - You are correct and you can update the Apache configuration to be more secure BUT Apache is only used for renewing the LetsEncrypt certs. #3 - The controller is using its own web server, that is why changing the settings in Apache does not help the ssl.com rating. I don't use 2FA on my controller - after all, it is locked down so it is only accessible from my network and I am not overly concerned with someone else getting into it. I also don't particularly like the idea of relying on Ubiquiti to log into a controller I own/manage.

Chis Murphy - November 25, 2020
Part III Update: Your pre and post firewall 80/443 scripts will disable the login page for those that have Single Sign On remote access turned on (usually to activate 2FA). Its a shame that the Unifi Controller does not have 2FA built in... then your pre/post firewall 80/443 scripts would be practical and the server would be more secure by cutting Ubiquiti Inc. out of the login process. Come to think of it... maybe Ubiquiti only uses one of these two ports, so your script can go on blocking the other port. Also... My suggestion to change the apache protocols did not result in an improved SSL Rating here: https://www.ssllabs.com/ssltest/analyze.html Unifi must be running its own instance of a web server?

Chis Murphy - November 24, 2020
Part II: Finally, a new issue came up which you can address on your blog post... https://www.ssllabs.com/ssltest/analyze.html tested my server and found soon to be deprecated TLS 1.0 and TLS 1.1 protocols. To disable them I went to: /etc/apache2/mods-enabled/ssl.conf I changed this: SSLProtocol all -SSLv3 to this: SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 Instructions then say to restart apache... so I think the whole controller needs to be restarted? (warning: this will interrupt the Wifi on your LAN) Instructions: https://www.ssl.com/guide/disable-tls-1-0-and-1-1-apache-nginx/

Chris Murphy - November 24, 2020
Hello and thanks for posting this. I got SSL working on port 9090 with your help. I did encounter a few issues. typo in letsencrypt: sudo nano /etc/letsenctrypt/renewal-hooks/pre/ufw_allow.shl In cockpit_import_ssl.shl you have the line: FQDN=`cat /etc/hostname`; that only returned the subdomain and not the full domain. So I changed it to: FQDN=unifi.example.com; #substitute your domain for example Then you have the lines sudo -i /etc/cron.daily/cockpit_import_ssl

Brian Snelgrove - May 25, 2020
Thank you Kym, those have been fixed. I am glad the article helped you!

Kym Busby - May 24, 2020
Hi Brian, Thanks so much for this guide, I was struggling to figure out how to achieve this myself. I did note a couple of typo's in one section that you may want to correct for others, to which I've added comments in CAPS : Lastly we should add an initialization script to the cron.daily so our certs get updated automatically: sudo nano /etc/cron.daily/cockpit_import_ssl #!/bin/bash /usr/local/inb/cockpit_import_ssl.shl **** PATH SHOULD BE "/usr/local/bin/cockpit_import_ssl.sh" not "/usr/local/inb/cockpit_import_ssl.sh" Make sure the file is owned by root and is made executable: sudo chown root.root /etc/cron.daily/cockpit_import_ssl such chmod +x /etc/cron.daily/cockpit_import_ssl **** COMMAND SHOULD BE "sudo" not "such" Again, thanks so much for this guide! Regards, Kym Busby