cloudkey is a replacement for /usr/bin/ck-ui on your Ubiquiti Cloud Key
Generation 2 device: a small Go daemon that drives the front-panel OLED
display and status LEDs directly, without the stock UI, with an optional web
dashboard. Works on UCK-G2 and UCK-G2-PLUS models.
The display cycles through these screens, fading in and holding each one lit before fading to a genuinely black, held-blank state (real rest time for the OLED panel) between screens.
Web Dashboard (opt-in, see -http-port below)
cloudkey (this repo, the resulting binary) replaces the front panel.
The wiki covers everything
else — a phase-by-phase runbook for turning a Cloud Key Gen2 / Gen2 Plus into
a plain Debian server and building on it. Verified on real hardware, written
so a stranger can follow along, and paired with ready-to-run scripts in
scripts/runbook/.
| Phase | What it gets you |
|---|---|
| 1 — De-Ubiquitizing | Strip the UniFi stack (keeping the packages that must stay), install cloudkey in place of ck-ui, format and mount the internal drive, harden to an admin user with key-only SSH |
| 2 — Apps | NZBGet, Sonarr, Radarr, Prowlarr on ARM, plus hardening — SQLite healing, liveness probes, surviving upgrades |
| 3 — AutoSSH | A reverse-SSH rescue tunnel through a relay, so you can still reach the box when the network moves out from under it (feeds the autossh screen above); macOS variant included |
| 4 — WireGuard | Fail-closed VPN egress using a network namespace — apps get no route at all if the tunnel drops, plus health checks and self-healing (feeds the wireguard screen) |
| 5 — Headscale | A self-hosted Headscale + headplane control server and client enrolment, including a Synology NAS (feeds the tailscale screen) |
| 9 — Backup & Restore | Back every custom file up to the SD card and put it back on a rebuilt box |
Also there: CloudKey Admin Tools, an on-box status dashboard and diagnostic runbook that works across every phase.
Wiki pages are edited in wiki/ in this repo and published by CI —
so send wiki fixes as a pull request, not through the wiki's web editor
(those edits get overwritten).
- Turn on SSH from the Unifi Console
ssh root@UniFi-CloudKeyG2- Download the latest
cloudkeybinary from the Releases page and copy it to/usr/local/bin/cloudkey. - Continue with Using the
systemdService below. It disables the stock service without removing its binary, so you can restore it if needed.
Disable the old service first.
systemctl disable ck-uisystemctl stop ck-ui
Do not skip this.
ck-uiandcloudkeyboth drive the OLED panel, and both want the front-panel reset button — but the button's event device (/dev/input/event1) can only be read usefully by one process. Leavingck-uirunning means the two fight over the panel, and the stock daemon's timed button behaviour (a documented factory reset on a long hold) stays live alongside cloudkey's own. Verify withfuser -v /dev/input/event1—cloudkeyshould be the only listed consumer.
Install this one.
- Copy
cloudkey.serviceto/lib/systemd/system/and ensure the downloaded or built binary is at/usr/local/bin/cloudkey. touch /etc/cloudkey.envand set any flags you need as environment variables (see Configuration below).systemctl daemon-reloadsystemctl enable cloudkeysystemctl start cloudkey
- Have a working Go environment (see
go.modfor the minimum version). make build— cross-compiles for the Cloud Key'slinux/armtarget and writes the binary to.local/bin/cloudkey.- Set
DEPLOY_HOST/DEPLOY_KEYin theMakefile(or pass them on the command line) for your device, then:make install— first-time setup: installs the systemd unit, creates/etc/cloudkey.envand the web root, enables the service, and deploys.make deploy— every time after: pushes a new binary and dashboard and restarts the service. It expects the unit to already exist, so runmake installonce first.
Both targets act on the remote device over SSH. make install is idempotent,
so re-run it after changing cloudkey.service.
At this point, you can choose to back up and overwrite the /usr/bin/ck-ui
file or install the systemd service above, depending on your Linux
experience.
Every flag can also be set via an environment variable — useful for
/etc/cloudkey.env under systemd. Env vars are the flag name uppercased,
with dashes replaced by underscores, prefixed with CLOUDKEY_.
| Flag | Env var | Default | Description |
|---|---|---|---|
-delay |
CLOUDKEY_DELAY |
5000 |
Milliseconds each screen stays lit |
-blank-delay |
CLOUDKEY_BLANK_DELAY |
3000 |
Milliseconds screens stay blanked between screens |
-demo |
CLOUDKEY_DEMO |
false |
Use fake screen data instead of network, storage, and CPU/memory collection; the framebuffer and LEDs still require target hardware |
-stealth-mode |
CLOUDKEY_STEALTH_MODE |
false |
Start with the front panel dark — no LEDs, no screens. A brief reset-button tap toggles it at runtime; see Stealth mode |
-pidfile |
CLOUDKEY_PIDFILE |
/var/run/cloudkey.pid |
Pidfile path |
-reset |
CLOUDKEY_RESET |
false |
Clear the screen and exit, instead of running normally |
-version |
CLOUDKEY_VERSION |
false |
Print version and exit |
-autossh-tunnel1-name |
CLOUDKEY_AUTOSSH_TUNNEL1_NAME |
"" |
Label for the first autossh tunnel; empty disables the autossh screen entirely |
-autossh-tunnel1-service |
CLOUDKEY_AUTOSSH_TUNNEL1_SERVICE |
"" |
systemd unit name to check for the first tunnel's liveness (e.g. autossh-tunnel1); empty always shows down |
-autossh-tunnel2-name |
CLOUDKEY_AUTOSSH_TUNNEL2_NAME |
"" |
Label for the second autossh tunnel; empty hides the second row |
-autossh-tunnel2-service |
CLOUDKEY_AUTOSSH_TUNNEL2_SERVICE |
"" |
systemd unit name to check for the second tunnel's liveness |
-wireguard-name |
CLOUDKEY_WIREGUARD_NAME |
WireGuard |
Display name for the WireGuard screen |
-wireguard-iface |
CLOUDKEY_WIREGUARD_IFACE |
"" |
WireGuard interface to check, e.g. wg0; empty disables the wireguard screen |
-wg-cmd |
CLOUDKEY_WG_CMD |
wg |
wg binary to run for WireGuard status checks; override if it's not on PATH |
-tailscale |
CLOUDKEY_TAILSCALE |
false |
Enable and display the Tailscale screen |
-tailscale-name |
CLOUDKEY_TAILSCALE_NAME |
TailScale |
Display name for the Tailscale screen |
-tailscale-cmd |
CLOUDKEY_TAILSCALE_CMD |
tailscale |
tailscale binary to run for Tailscale status checks; override if it's not on PATH |
-http-port |
CLOUDKEY_HTTP_PORT |
0 |
TCP port for the web dashboard + /events SSE stream; 0 disables it. Port 80 needs root or cap_net_bind_service |
-web-root |
CLOUDKEY_WEB_ROOT |
/usr/share/cloudkey/website |
Directory of dashboard static files served at / |
-apps |
CLOUDKEY_APPS |
"" |
Comma-separated local apps to show on the dashboard, each name:port (e.g. Grafana:3000,Sonarr:8989); an app is up when its port is LISTENing in any network namespace on this box |
If the wireguard or tailscale screen is enabled but its binary can't be found, cloudkey exits at startup with an error rather than silently showing "disconnected" forever — a missing dependency is a configuration error to fix, not a display state.
Setting CLOUDKEY_HTTP_PORT starts an HTTP server that serves the responsive
web dashboard (the files under website/) and a live Server-Sent Events
stream at /events. The same collector timers that drive the OLED carousel also
push their readings to the browser, so the dashboard updates without polling.
The stream is incremental: a browser gets one full snapshot on connect, then
each timer pushes only its own slice (a WireGuard tick sends just that tunnel, a
CPU tick just CPU). Tunnels carry live transfer counters and latency where the
source exposes them — WireGuard rx/tx and handshake age from wg show <iface>,
Tailscale rx/tx from tailscale status --json, plus an ICMP ping to the peer.
The dashboard tunnel cards show RX, TX, and either Ping when connected or Last
Seen when disconnected.
The -apps list adds simple tiles below the tunnels. Each entry is name:port
for an app on the Cloud Key itself. On Linux, cloudkey reads this box's TCP
LISTEN tables across every network namespace and marks an app up when that port
is listening; where those tables are unavailable, it falls back to a loopback
TCP dial. Dashboard links use the same scheme and hostname currently showing
the dashboard, with only the port changed. Provide as many as you like; names
may not contain : or ,.
Binding port 80 requires privilege — run as root (the service already does) or grant the binary the capability once:
sudo setcap cap_net_bind_service=+ep /usr/local/bin/cloudkeyThe cloudkey daemon always drives the front panel: boot logo, loader animation, a steady blue "running" LED, and the OLED carousel rotating forever. There is no way to make the device go dark — useful when the box sits in a bedroom or any shared space where a glowing panel is unwanted.
CLOUDKEY_STEALTH_MODE (default false) suppresses all front-panel
output — LEDs and OLED both — toggled at runtime by a physical reset-button
tap. Engaging shows a Stealth Mode / ENGAGED banner for 5 seconds so you know
the press registered, then the panel goes fully dark. Disengaging is a silent
resume: LEDs return and the carousel restarts mid-rotation, no banner.
Stealth is display-only. The collector timers and the web dashboard / SSE stream keep running untouched — a dark panel must not mean a dead device.
Two things worth knowing before you go looking for a bug:
- The tap has to be brief — roughly a tenth to half a second. A longer hold lands in a different press band and does nothing at all, which is easy to mistake for a broken button. The band cloudkey classified each press into is logged, so check the journal if a press seems ignored.
- The flip is not persisted. Restarting the service returns to whatever
CLOUDKEY_STEALTH_MODEsays, so set it there if you want dark to be the default.
WireGuard and Tailscale check actual tunnel liveness rather than the
systemd unit state of the underlying service: wg-quick@<iface> is
typically a one-shot unit that stays "active" forever once wg-quick up
succeeds, and tailscaled's unit reports "active" whenever the daemon
process is running, neither of which reflects the peer/tailnet actually
being reachable. So WireGuard status comes from the human-readable
wg show <iface> output (recent handshake age plus transfer counters), and
Tailscale status comes from tailscale status --json's BackendState.
autossh is the exception: -R (remote) forwards bind no local port to
probe, so without a -M monitor port there's no local traffic signal to
check at all. autossh status therefore falls back to systemctl is-active <unit> — which only proves the autossh/ssh process is still running, not
that the tunnel is passing traffic. Pair each tunnel's ssh config with
ServerAliveInterval/ServerAliveCountMax so a genuinely dead connection
makes the process exit (and the unit go inactive) instead of hanging open
indefinitely; without that, a stale tunnel can still show as "up".
The blue and white LEDs are one status indicator, not two: white alone means powered but not running (during boot, and after the service stops), blue alone means running. After boot the status LED is therefore blue while the daemon is idle. In stealth mode both are dark, including on shutdown — see Stealth mode.
The reset button is read as a plain evdev key and classified by how long it was held, measured key-down to key-up. Only a brief tap does anything today; the longer bands are reserved and deliberately separated by dead zones, so releasing between bands is always a safe no-op. Nothing fires past six seconds.
CLOUDKEY_WIREGUARD_NAME is just the label shown as the screen's title —
pick anything (it defaults to WireGuard). CLOUDKEY_WIREGUARD_IFACE
is not free text: it has to match the real interface name on the device
running cloudkey. To find it, run one of these on that device:
wg show interfaces # lists every active WireGuard interface
ip link show type wireguardIf the tunnel is managed by wg-quick (as wg-quick@<iface> under
systemd), the interface name is also the config file's name minus
.conf — /etc/wireguard/wg0.conf means wg0 — or you can read it
straight off the unit:
systemctl list-units 'wg-quick@*'See cloudkey.env.example for a starter /etc/cloudkey.env. Example:
CLOUDKEY_STEALTH_MODE=true
I am an edge case. I do not use my Cloud Key device for Unifi. I think it is a great sexy little hardware device, but to manage a network off of what is essentially a POE SDCard, you are insane.
Issues with stability are very well documented. Using mongodb on an sdcard (limited write cycles) without automatically reparing has lead me to have to recover 4 times in 2 years even with the secondary USB power from the UPS. That is NOT remotely production stable. Run Unifi on a server, not a "raspberry pi".
With that said, I am sure you are asking yourself "Why do you have it all?" The Ubiquity Cloud Key Gen2 is a POE, ARMv7, Single-Board-Computer with on-board battery backup and a 160x64 framebuffer display built-in. It is sexy, for under $150. It looks like an iDevice.
Sure, you can buy a $35 Raspberry Pi, add a case, with a touchscreen, with a power-supply, and blah blah, but I'll pay for quality and craftmanship so it does not look like another Frankenstein project around my house.
I can ship it to my parents, tell them to plug one cable into the new-fangled doo-hickey and tell them to call their ISP when it has a sad face on it (feature not developed yet).







