A captive portal for UniFi guest Wi-Fi: serverless, with a tiny redirector as the bridge
UniFi only accepts an IPv4 address as its external portal server, and CloudFront has no static IP. A stateless micro-redirector in front of a serverless backend closes the gap – a pattern for any vendor constraint of this kind.
For the open guest Wi-Fi at one of my sites I wanted my own sign-in page: accept the terms of use, get the device authorised, done. The network runs on UniFi. The case is small, but the pattern comes up with clients all the time: a vendor product imposes a condition that doesn’t fit the architecture, and you need the thinnest possible bridge. A use case, not a showcase.
Hypothesis
A captive portal for UniFi can run almost entirely serverless – static page, API, function, counter table – without a stateful server to look after. The one obstacle I knew about up front: in the “External Portal Server” field, UniFi accepts nothing but an IPv4 address. A CDN like CloudFront has no static IP. My assumption was that a tiny, stateless redirector with a fixed address would close that gap and have nothing else to do.
Three questions: Does the redirect chain from UniFi to the page work? Does the backend reach the gateway through the vendor’s cloud connector? And does the non-serverless part stay low-maintenance?
Setup
The flow has four stops:
- UniFi intercepts the first HTTP request from a guest who isn’t authorised yet and forwards it to the configured IPv4 address with the device identifier and target URL as query parameters.
- The redirector – an ARM micro instance (t4g.nano) running nginx with a fixed public address – answers every request with a 302 to the HTTPS portal page and passes the query string through unchanged.
- The portal page is a static site in S3 behind CloudFront. API calls go through the same origin to an HTTP API gateway and on to a Lambda function.
- The Lambda function talks to the gateway via the UniFi Site Manager cloud connector: it checks whether the device is currently connected as a guest on the hotspot network and, once the terms are accepted, authorises it.
A few deliberate choices:
- The redirector is disposable. No SSH, access via SSM only, no data on disk, security updates applied automatically. If its boot configuration changes, CDK replaces the instance rather than patching it in place. Only the static address survives, because UniFi points at it.
- Data minimisation. The query string carries the guest’s device identifier, so the nginx access log is switched off. The Lambda logs are deleted after 14 days.
- No identity service. Open guest Wi-Fi; the only hurdle is accepting the terms.
- The page only shows the sign-in when it makes sense. On load it asks the backend whether the device really is a guest on the hotspot. Anyone opening the page from another network just sees a notice. The authorise call re-checks the same condition server-side.
- Rate limits without IP addresses. All guests share one WAN address, so a per-IP limit would be pointless. Instead the function keeps counters in DynamoDB per device and globally, each with a per-minute cap, with a throttle at the API gateway stage in front.
- Time-limited access. A guest gets 8 hours; after that the page appears again.
Everything, including certificate and redirector, is CDK code. Only the cloud-connector API key (in a secret) and the UniFi hotspot settings live outside it.
Measurement
I didn’t run a load test – for guest Wi-Fi it would say little. I checked the functional chain and the pitfalls:
- The IPv4 constraint is real. UniFi rejects a hostname in the “External Portal Server” field with a validation error.
- The redirect chain holds. A request to the redirector with a test device identifier returns the expected 302 to the portal page with the query string intact; the page renders.
- The path to the gateway works. The status check for a made-up device identifier returns “not connected”. That proves Lambda, secret and cloud connector work together; a missing or wrong key would show up as an upstream error.
- Operation. The portal has been in production since early October.
Two lessons learned:
- Even the smallest instance needs care. With 512 MiB of memory, the package installation on first boot was killed by the OOM killer. Since then the boot script creates a 1 GiB swap file before anything else.
- Caching on static portal pages. After a CSS and JavaScript change, some devices still saw the old version. The assets now carry a version number in their URL that is bumped on every change.
On cost: most of the running cost is the micro instance and its public IPv4 address, each in the range of a few dollars a month. The serverless part is negligible at guest-traffic volumes. The redirector runs around the clock – stop it at night and guests get no sign-in page.
Conclusion
Hypothesis confirmed, with one caveat: it can’t be done entirely without a server, because the vendor insists on a fixed IPv4. But that server is reduced to the bare minimum – one redirect, no state, no data, replaceable at any time. The logic stays serverless, versioned and testable.
The pattern carries beyond Wi-Fi: when a legacy system, device or partner only accepts a fixed IP, only HTTP or one particular format, a thin, stateless adapter at exactly that point beats bending the whole architecture around the constraint. For this case I’m sticking with the micro instance: it patches itself, and any configuration change simply replaces it.
Stack: AWS CDK · CloudFront · S3 · API Gateway · Lambda · DynamoDB · EC2 · nginx · UniFi