A little while ago I migrated my UniFi Controller to Kubernetes, part of that process involved migrating my WPA2 Enterprise WiFi network in to the cluster. It’s quite an involved process and not one I’ve seen anyone try to do, so this post is going to look at how you can do that integration…as well as some of the reasons you might not want to do it in the real world! This is probably the most niche thing I’ve ever written and I struggle to imagine that anyone other than me will ever want to do such a thing…but hopefully someone else will find this useful one day 🙂
The configs for this post can be found in GitHub here.
This project has 4 main requirements:
- A Certificate Authority which can issue TLS certificates. I have already covered setting up a PKI within Kubernetes using cert-manager, and that’s what I’m going to be using in this example.
- A RADIUS server which can function as part of a PKI with our Certificate Authority.
- A LoadBalancer controller (I will also be exposing our RADIUS server via an external LoadBalancer, for reasons we’ll come back to), we’ll be using MetalLB, I have already covered the setup of MetalLB here. Any LoadBalancer is fine.
- The UniFi Controller.
Configuring freeradius
I’m going to be building on the deployment method suggested by freeradius-k8s by stanislaspiron. It is a wrapper around the official freeradius image that works well and means we don’t have to go re-inventing the wheel too much.
First we’ll need to configure our freeradius config files as below:
#--eap eap { default_eap_type = tls timer_expire = 60 ignore_unknown_eap_types = no cisco_accounting_username_bug = no max_sessions = ${max_requests} tls-config tls-common { private_key_file = /etc/ssl/private/tls.key certificate_file = /etc/ssl/certs/tls.crt ca_file = /etc/ssl/certs/ca.crt dh_file = ${certdir}/dh random_file = /dev/urandom ca_path = ${cadir} cipher_list = "DEFAULT" ecdh_curve = "prime256v1" cache { enable = no } ocsp { enable = no } } tls { tls = tls-common } }
#--radiusd.conf prefix = /opt exec_prefix = ${prefix} sysconfdir = ${prefix}/etc localstatedir = ${prefix}/var sbindir = ${exec_prefix}/sbin logdir = ${localstatedir}/log/radius raddbdir = ${sysconfdir}/raddb radacctdir = ${logdir}/radacct name = radiusd confdir = ${raddbdir} modconfdir = ${confdir}/mods-config certdir = ${confdir}/certs cadir = ${confdir}/certs run_dir = ${localstatedir}/run/${name} db_dir = ${raddbdir} libdir = ${exec_prefix}/lib pidfile = ${run_dir}/${name}.pid correct_escapes = true max_request_time = 30 cleanup_delay = 5 max_requests = 16384 hostname_lookups = no log { destination = stdout colourise = yes file = ${logdir}/radius.log syslog_facility = daemon stripped_names = no auth = yes auth_badpass = no auth_goodpass = no msg_denied = "You are already logged in - access denied" } checkrad = ${sbindir}/checkrad ENV { } security { allow_core_dumps = no max_attributes = 200 reject_delay = 1 status_server = yes allow_vulnerable_openssl = no } proxy_requests = yes $INCLUDE proxy.conf $INCLUDE clients.conf thread pool { start_servers = 5 max_servers = 32 min_spare_servers = 3 max_spare_servers = 10 max_requests_per_server = 0 auto_limit_acct = no } modules { $INCLUDE mods-enabled/ } instantiate { } policy { $INCLUDE policy.d/ } $INCLUDE sites-enabled/
#--authorize. UPDATE THE PASSWORDS TO SOMETHING OF YOUR CHOSING rad_adm Cleartext-Password := "supersecretpasswordforadmins" F5-LTM-User-Role = 0, F5-LTM-User-Info-1 = mgmt, F5-LTM-User-Partition = Common, F5-LTM-User-Shell = tmsh rad_guest Cleartext-Password := "supersecretpasswordforguests" F5-LTM-User-Role = 700, F5-LTM-User-Info-1 = mgmt, F5-LTM-User-Partition = Common, F5-LTM-User-Shell = tmsh
#--clients.conf client localhost { ipaddr = 10.244.0.0/16 #--Set this to the IP range of your pod network secret = somelongandcomplicatedstringofrandomgibberish #--Update this to something of your choice require_message_authenticator = no nastype = other }
Note: Pay particular attention to the ipaddr attribute in your clients.conf. As RADIUS requests are going to be coming from the UniFi controller and this could be running on any Pod, you will need to consider the entire address space that your Pods could be running within. As I am using an overlay network I have allowed the entire overlay subnet, this is far from ideal and presents a significant security issue if running in the real world.
Deploying freeradius
With these configuration files set up, let’s deploy freeradius:
#--freeradius.yaml kind: Deployment apiVersion: apps/v1 metadata: name: freeradius namespace: freeradius spec: replicas: 2 selector: matchLabels: name: freeradius template: metadata: name: freeradius labels: name: freeradius spec: volumes: - name: raddb-files configMap: name: freeradius-files optional: true - name: raddb-secrets secret: defaultMode: 420 secretName: freeradius-secrets - name: radius-server-certificate secret: defaultMode: 420 secretName: freeradius-server-certificate containers: - name: freeradius image: 'freeradius/freeradius-server:latest-alpine' ports: - containerPort: 1812 protocol: UDP - containerPort: 1813 protocol: UDP volumeMounts: - name: raddb-secrets mountPath: /etc/raddb/mods-config/files/authorize subPath: authorize readOnly: true - name: raddb-secrets mountPath: /etc/raddb/clients.conf subPath: clients.conf readOnly: true - name: raddb-files mountPath: /etc/raddb/radiusd.conf subPath: radiusd.conf readOnly: true - mountPath: /etc/raddb/mods-enabled/eap name: raddb-files readOnly: true subPath: eap - mountPath: /etc/ssl/certs/ca.crt name: radius-server-certificate readOnly: true subPath: ca.crt - mountPath: /etc/ssl/certs/tls.crt name: radius-server-certificate readOnly: true subPath: tls.crt - mountPath: /etc/ssl/private/tls.key name: radius-server-certificate readOnly: true subPath: tls.key --- kind: Service apiVersion: v1 metadata: name: lb-freeradius namespace: radius annotations: metallb.universe.tf/allow-shared-ip: 'true' spec: ports: - name: 'radius' protocol: UDP port: 1812 targetPort: 1812 - name: 'radius-acct' protocol: UDP port: 1813 targetPort: 1813 selector: name: freeradius type: LoadBalancer loadBalancerIP: 10.0.0.199 #--Set as appropriate for your loadbalancer
Note here that we are exposing the service as a LoadBalancer which means exposing it to services outside of the Kubernetes cluster. This is not exactly desirable as the only client we want to interact with our RADIUS is UniFi and this would be ideally done via the Pod network the same as our client requests, however, the UniFi console only allows us to configure a RADIUS connection via a single IP address. This rules out using a ClusterIP which would be preferable as we cannot make our requests via hostname, less than ideal and it will add latency to each of our requests.
#--server-certificate.yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: freeradius namespace: freeradius spec: isCA: false subject: organizations: - tinfoilcipher organizationalUnits: - Ops countries: - GB dnsNames: - freeradius.freeradius.svc.cluster.local #--Include ClusterIP in SAN - freeradius.tinfoilcipher.co.uk #--Update as suitable for your domain secretName: freeradius-server-certificate usages: - client auth privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer #--Update issuer details kind: ClusterIssuer #--as suitable to group: cert-manager.io #--your cluster
kubectl create ns freeradius # namespace/freeradius created kubectl create configmap -n freeradius \ freeradius-files --from-file eap --from-file radiusd.conf # configmap/freeradius-files created kubectl create secret generic -n freeradius \ freeradius-secrets --from-file authorize --from-file clients.conf # secret/freeradius-secrets created kubectl apply -f server-certificate.yaml # certificate.cert-manager.io/freeradius created kubectl apply -f freeradius.yaml # deployment.apps/freeradius created # service/lb-freeradius created
and finally verify that our server is up and running:
kubectl get po -n freeradius # NAME READY STATUS RESTARTS AGE # freeradius-5db9ab96b4-rbf12 1/1 Running 0 4m kubectl get svc -n freeradius # NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE # freeradius LoadBalancer 10.107.204.88 10.0.0.199 1812:31508/UDP,1813:30370/UDP 4m
Configure The UniFi Controller
Within the UniFi console, configure a RADIUS profile under Settings > Profiles and configured as below, noting that:
- The IP Address should correspond to the freeradius Service External IP as shown above
- The Shared Secret should correspond to the secret configured in client.conf earlier
With the profile set up, ensure that it is attached to a SSID using WPA Enterprise:
Issuing Client Certificates
With the PKI, RADIUS and WPA Enterprise SSID now all in place, we can finally start connecting WiFi clients. To issue a new client certificate:
#--client-certificate.yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: somelaptop namespace: client-certificates spec: isCA: false duration: 26280h #--1 year. Really this is too long but practical, as subject: #--we have no innate means of rotating certificates. organizations: - tinfoilcipher organizationalUnits: - Ops countries: - GB dnsNames: - somelaptop.tinfoilcipher.co.uk secretName: somelaptop usages: - client auth privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: ClusterIssuer group: cert-manager.io
kubectl create namespace client-certificates kubectl apply -f client-certificate.yaml # certificate.cert-manager.io/somelaptop created kubectl get secret -n client-certificates somelaptop-certificate -o json | jq -r '.data."tls.crt"' | base64 -d >> somelaptop.crt kubectl get secret -n client-certificates somelaptop-certificate -o json | jq -r '.data."tls.key"' | base64 -d >> somelaptop.key kubectl get secret -n client-certificates somelaptop-certificate -o json | jq -r '.data."ca.crt"' | base64 -d >> ca.crt openssl pkcs12 -export -in somelaptop.crt -inkey somelaptop.key -name somelaptop.tinfoilcipher.co.uk -CAfile ca.crt -out somelaptop.p12 # Enter passphrase: *************************** rm ca.crt somelaptop.key somelaptop.crt
This process will produce a PKCS12 file (or .p12 file) which can be used by most clients to connect to your newly created WPA Enterprise network. That was simple wasn’t it!