The Ubiquity UniFi line of network devices has long been a top choice among small businesses and networking enthusiasts for their WiFi access points, switches, and more. One of the key aspects of managing these devices is the requirement for a controller, which can be installed on a separate device or self-hosted. In this blog post, I’ll guide you through the process of hosting your Ubiquiti UniFi controller in a Kubernetes cluster while utilizing Ingress Nginx to make it accessible over the internet.
First we create the Kubernetes Stateful Set which runs the application and then take a closer look at how traffic reaches it. In my case I needed to also migrate the data from my old controller instance and update the inform URL.
Running the Application
The application is deployed as a Kubernetes Stateful Set which is centred around the awesome Docker image created by Jacob Alberty.
To improve security, various options are set such as running the container as a non-root user.
The image uses the user with the ID 999 and an init-container is used to make sure that the storage is accessible to this user.
A PVC takes care of the storage needed by the UniFi Controller application, in my case the storage is provided by Longhorn.
The UniFi SDN Controller relies on MongoDB for data storage.
The great news is that the Docker image we’re using comes with an integrated MongoDB instance. This means there’s no need for a separate MongoDB deployment, simplifying our deployment.
The configuration of the Docker image is done via environment variables, however there is not much to configure.
It is recommended to set a timezone via the variable TZ
and the application is also configured to log to stdout (also known as the console) in addition to a log file.
This is especially helpful when using a centralized logging Platform such as ElasticSearch or Grafana Loki.
Finally, the most important ports are defined.
These are 8443
for the Web UI via HTTPS, 8080
which can be used to connect UniFi devices via HTTP and 3478
for the STUN traffic which is used for example for the interactive console in the Web UI.
Overall pretty standard for an application in Kubernetes.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: unifi
spec:
selector:
matchLabels:
app: unifi
serviceName: unifi
replicas: 1
template:
metadata:
labels:
app: unifi
annotations:
container.apparmor.security.beta.kubernetes.io/unifi: runtime/default
seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 999:999 /unifi"]
securityContext:
runAsUser: 0
runAsNonRoot: false
volumeMounts:
- name: unifi
mountPath: /unifi
containers:
- name: unifi
image: jacobalberty/unifi:v7.4.162
env:
- name: TZ
value: EUROPE/BERLIN
- name: UNIFI_STDOUT
value: "true"
ports:
- containerPort: 8080
name: inform
- containerPort: 8443
name: ui
- containerPort: 3478
name: stun
protocol: UDP
volumeMounts:
- name: unifi
mountPath: /unifi
- name: tmp
mountPath: /tmp
resources:
requests:
memory: 1Gi
cpu: 250m
limits:
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
volumes:
- name: tmp
emptyDir: {}
- name: unifi
persistentVolumeClaim:
claimName: unifi
automountServiceAccountToken: false
Get Traffic to the Application
Making the UniFi Controller accessible from the internet involves a few intricacies, setting it apart from many other web applications.
This is because a Kubernetes Ingress can only serve HTTP(S) traffic.
Consequently, traffic on port 8080
and especially the UDP traffic for the STUN protocol would necessitate a service of the type LoadBalance to be reachable from outside the cluster.
However, the Ingress Nginx allows for a workaround, allowing us to configure and expose ports other than the standard 80/443 for HTTP(S).
Nonetheless, an internal service has to be created which makes the application reachable inside the cluster.
Of course it needs to be configured with all three ports, but don’t forget the field name
and also set the field protocol
for the traffic over the port 3478
, like so:
ports:
- port: 3478
targetPort: 3478
protocol: UDP
name: stun
An Ingress Object is needed to access the Web UI of the UniFi Controller. For the Ingress to function properly, some additional configuration is needed which is specific to the Nginx Controller used for Ingress. This is done by setting some annotations.
The first annotation is necessary because the UniFi controller application does not expose the Web UI via HTTP but rather secured by TLS via HTTPS. If the annotation is not set, you will get the error “Bad Request This combination of host and port requires TLS.”
The second annotation is necessary if you plan to restore the application from a backup file. In the default configuration the maximum body size of POST requests is restricted. Setting the body size to 0 with this annotation effectively allows for an “unlimited” body size.
Finally, I also had to add some additional configuration snippets to the Nginx Ingress. This is because in my specific setup there is an additional reverse proxy between the client and the Kubernetes Ingress. Without the settings for the headers Origin and Referer, the Web UI of the UniFi SDN controller would not work properly.
With the manifest below, the Web UI will be reachable from outside the cluster. However, it’s worth noting that additional ports will also need to be exposed, which we’ll explore in the upcoming section.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: unifi
annotations:
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Origin '';
proxy_set_header Referer '';
spec:
ingressClassName: nginx
tls:
- hosts: [ "unifi.${DOMAIN}" ]
secretName: unifi-cert
rules:
- host: unifi.${DOMAIN}
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: unifi
port:
number: 8443
Although a Kubernetes Ingress object can only forward HTTP(S) traffic, it is possible to expose TCP and UDP services via the Ingress Nginx controller.
If it is installed via a Helm Chart this is even more straightforward.
Simply add a mapping between the exposed ports and the service which should be exposed within the Helm Chart’s values.
This can be done by adding entries to the respective udp
or tcp
sections.
udp:
"3478": "default/unifi:3478"
tcp:
"8080": "default/unifi:8080"
Now all the necessary ports for the UniFi SDN controller are exposed via the Ingress Nginx.
Migration and Final Steps
Finally, existing data can be migrated from the old controller according to the documentation offered by Ubiquity.
If the hostname or IP address of your controller has changed, you might need to update the configuration on some of your devices.
Therefore, SSH into the device using the credentials provided under Settings > Site > Device Authentication.
Then, use the command set-inform http(s)://<url>/inform
to update the IP or hostname of the controller.
If you read this post, liked it, found it helpful or if you have some criticism, please leave a comment below.