A lightweight web UI for managing the allowlist on UniFi's ad-blocking content filter. Designed so family members on your local network can greenlight a website without needing access to the UniFi controller. Full disclosure, this is a vibe coded solution for personal use 🤖
- Add or remove domains from the content filter allowlist
- Simple, mobile-friendly dark UI — no login required
- Supports multiple content filter profiles (auto-detected)
- API key or username/password authentication to UniFi
- Hardened Docker container (non-root, read-only FS, no capabilities)
- UniFi OS >= 3.x
- UniFi Network >= 8.2.93
git clone <your-repo-url>
cd greenlight
cp .env.example .envEdit .env with your UniFi controller details:
UNIFI_HOST=https://192.168.1.1
UNIFI_SITE=default
# Option 1: API key (preferred)
UNIFI_API_KEY=your-key-here
# Option 2: username/password
UNIFI_USERNAME=admin
UNIFI_PASSWORD=changemedocker compose up -d --buildOpen http://<your-server>:3000 in a browser.
| Variable | Required | Default | Description |
|---|---|---|---|
UNIFI_HOST |
Yes | — | UniFi controller URL (e.g. https://192.168.1.1) |
UNIFI_API_KEY |
No* | — | API key (create in UniFi OS > Admins > API Keys) |
UNIFI_USERNAME |
No* | — | Admin username |
UNIFI_PASSWORD |
No* | — | Admin password |
UNIFI_SITE |
No | default |
UniFi site name |
FILTER_ID |
No | — | Restrict UI to a specific filter ID |
PORT |
No | 3000 |
App listen port |
* Either UNIFI_API_KEY or both UNIFI_USERNAME and UNIFI_PASSWORD must be set.
Greenlight supports 2 styles of authentication:
- UniFi API Key
- Username & Password (Deprecated)
Click the below headers to view the instructions:
UniFi API Key — Network v9.0.0+
API key authentication is the recommended method. It avoids session management overhead, doesn't expire on idle, and works with UniFi Network v9.0.0 and later.
-
Open your UniFi controller/Console's admin page either via unifi.ui.com or via the IP address of your controller
-
On the left navigation bar (that runs the length of the page) click the people icon (
Admin & Users) -
Click
+ Create Newat the top of the page and fill it out using the below details
| Field Name | Value |
|---|---|
| First name | Greenlight |
| Last name | UnifiApi |
| Admin | ✅ |
| Restrict to local access only | ✅ |
| Username | greenlight |
| Password | Make up a password, but make note of it |
| Use a pre defined role | ✅ |
| Role | Super Admin |
Your user should now look like the below
-
Login to your console as the user you have just created. This will need to be done via the controller's IP address
-
On the left navigation bar (that runs the length of the page) click the
icon (
Integrations)
Give the API key a name, something like greenlight
Copy this Key, we will need it later. Your page should now look like the below
- Remove elevated permissions from the user
Log back in as your normal account, head over to where we created the Greenlight UnifiApi account
(On the left navigation bar (that runs the length of the page) click the people icon (Admin & Users))
Open that account, click the Gear Icon then match the below
We have unselected Use a Predefined Role and changed the ufo icon to Site admin and the person to None
You're probably thinking wow, that was long, and it's because only super admins can create API Keys, but they do not need
those permissions the entire time to be able to have API Key attached to that user. It's a bug feature in UniFi
The Site Admin permissions are more than enough to allow that user to create and manage records in our controller
- Create a Kubernetes secret called
greenlight-secretthat will hold yourUNIFI_API_KEYwith their respected values from Step 3.
---
apiVersion: v1
kind: Secret
metadata:
name: greenlight-secret
stringData:
UNIFI_API_KEY: <your-api-key>UNIFI_HOST=https://192.168.1.1
UNIFI_API_KEY=your-key-hereUsername & Password (Deprecated)
[!WARNING] Username/password authentication is deprecated and may be removed in a future release. Migrate to API key authentication when possible.
Set UNIFI_USERNAME and UNIFI_PASSWORD in your .env. The app manages login sessions automatically and re-authenticates when the session expires.
UNIFI_HOST=https://192.168.1.1
UNIFI_USERNAME=admin
UNIFI_PASSWORD=changemeThe container defaults to UID/GID 1000. Override at build or runtime:
# Build-time
docker build --build-arg APP_UID=65534 --build-arg APP_GID=65534 -t greenlight .
# Docker Compose (via environment or .env)
APP_UID=65534 APP_GID=65534 docker compose up -d --buildFor Kubernetes, use securityContext:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
allowPrivilegeEscalation: falseThe Docker container follows least-privilege principles to minimize blast radius:
- Non-root user — runs as UID/GID 1000 by default (configurable via build args)
- Read-only filesystem — only
/tmpis writable; no persistent state in the container - All capabilities dropped —
cap_drop: ALL, no Linux kernel capabilities granted - No privilege escalation —
security_opt: no-new-privilegesprevents setuid/setgid binaries - Self-signed TLS — accepted for UDM connections (standard for local UniFi controllers)
All user input is validated and sanitized at multiple layers to prevent XSS, injection, and abuse:
- Strict domain regex — the backend only accepts domains matching
^[a-z0-9]([a-z0-9-]*\.)+[a-z]{2,}$, rejecting any input containing JavaScript, HTML, special characters, or path traversal attempts - Input normalization — domains are lowercased with protocols,
www.prefixes, paths, and whitespace stripped before validation - Type and length enforcement — domain input must be a string of 253 characters or fewer (the DNS maximum)
- Filter ID validation — route parameters are validated against MongoDB ObjectId / UUID formats, preventing path manipulation in upstream API calls
- HTML escaping — all domain values are escaped before rendering in the frontend
- Content Security Policy — Helmet enforces strict CSP headers with per-request script nonces, blocking inline script injection even if a value were somehow rendered unescaped
- Rate limiting — API endpoints are capped at 30 requests/minute to prevent brute-force or spam abuse
- JSON-only API — the server only accepts
application/jsonrequest bodies, eliminating form-based CSRF vectors
Note: Domain values are sent to the UniFi controller as string elements in a JSON array. They are never interpolated into shell commands, URL paths, or database queries — there is no path from the domain input to command execution on the router.
The app uses UniFi's undocumented v2 content-filtering API:
- Authenticate —
POST /api/auth/login(or API key header) - List filters —
GET /proxy/network/v2/api/site/{site}/content-filtering - Update filter —
PUT /proxy/network/v2/api/site/{site}/content-filtering
The update endpoint auto-negotiates the correct method/path for your firmware version.
greenlight/
├── src/
│ ├── server.js Express server + API routes
│ └── unifi-client.js UniFi API client
├── public/
│ ├── index.html Single-page UI
│ ├── style.css Dark theme styles
│ └── app.js Frontend logic
├── Dockerfile Multi-stage, non-root
├── docker-compose.yml Production-ready config
├── .env.example Configuration template
└── package.json
MIT




