# Building a LAN-Only KVM

> Compiling a custom NanoKVM Lite image without remote access

By Zsolt Bizderi · Published 2026-06-17
Canonical: https://ambientnode.uk/building-a-lan-only-nanokvm-lite

The [NanoKVM Lite](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/introduction.html) is a cheap RISC-V IP-KVM based on the LicheeRV Nano. It captures a host's HDMI output, presents itself to that host as a USB keyboard and mouse, and can mount virtual media. This gives you full control over whatever machine it is plugged into.

![](/media/e03c4e40-da84-4333-b988-8e97cb73a748/691.webp)

It also ships with a vendor image I did not want to run as-is, because it bundles Tailscale. Convenient if you want remote access without running your own VPN, but it means the device reaches out to a third-party coordination server to function, which means the KVM can turn into a significant vulnerability and backdoor.

The firmware has a history of weak defaults with SSH enabled using a `root:root` login, a hardcoded key reused across devices for encrypt operations, and missing CSRF protection on the API. Most of this has been fixed since, but the track record is enough to distrust the default image.

So I built my own image using a fork that only works on the LAN, with no path to the outside compiled into it.

This post assumes familiarity with Linux and cross-compilation, and that you have written an image to an SD card before. The build is done on an x86 Linux host, not on the device.

## Building vs Firewalling

I do firewall it as well, and you should too. Deny-by-default egress on an isolated VLAN, but the device not having the capability at all to reach outside AND firewall blocking traffic are layers that support each other, not alternatives to each other.

[Tailscale](https://ambientnode.uk/tailscale-vpn) is good, and for some it may be a feature that solves remote access if they do not run their own VPN, but a KVM can be a major single point of compromise for everything it's plugged into.

I already run [WireGuard](https://ambientnode.uk/wireguard-vpn), terminating on my own router. My access model does not need anything from the device:

* On the LAN, I hit the KVM's local IP.
* Remote, I tunnel into my network, then hit the same local IP.

As far as the KVM is concerned those two are identical, both are just a host on the LAN. For this setup Tailscale is redundant attack surface.

## The build

The work is already mostly done by the [scpcom/LicheeSG-Nano-Build](https://github.com/scpcom/LicheeSG-Nano-Build) fork. It builds the NanoKVM server and web frontend from source, uses a reproducible toolchain, and defaults Tailscale off. When Tailscale is disabled it also strips the egress pieces out of the final rootfs, including the `tailscale`/`tailscaled` binaries, the Tailscale init scripts, the SSDP discovery daemon, and `dnsmasq` and `avahi`.

What is left brings up its ethernet interface, listens for the web UI on the LAN, and does not initiate anything outbound.

### Host setup

Install the dependencies needed to build the image:

```
sudo apt update
sudo apt install -y \
  cpio xxd build-essential cmake git pkg-config rsync unzip wget zip \
  bc bison flex liblzma-dev libncurses-dev libssl-dev device-tree-compiler \
  autoconf automake libtool ninja-build tcl \
  dosfstools file mtools fuse2fs shellcheck \
  python3 python-is-python3 python3-git python3-jinja2 python3-requests
```

### Building

The first build creates the base board image (toolchain, u-boot, kernel, middleware, rootfs). The second adds the NanoKVM application and produces the image you flash.

```
git clone https://github.com/scpcom/LicheeSG-Nano-Build --depth=1
cd LicheeSG-Nano-Build
git submodule update --init --recursive --depth=1

./build-licheervnano.sh
# wait for OK, then:
./build-nanokvm.sh --no-tailscale
```

Compiling can take a while, so go grab a coffee.

You end up with two `.img` files under `~/.../LicheeSG-Nano-Build/install/soc_sg2002_licheervnano_sd/images`. Flash the one from `build-nanokvm.sh`, the newer of the two. The base image will boot but has no KVM application on it.

## Verifying the clean image

Now to confirm all egress and Tailscale have been stripped out:

```
IMG=$(ls -t install/soc_sg2002_licheervnano_sd/images/*.img | head -1)
LOOP=$(sudo losetup -fP --show "$IMG")

mkdir -p /tmp/kvmroot
for part in ${LOOP}p*; do
  sudo mount -o ro "$part" /tmp/kvmroot 2>/dev/null || continue
  [ -d /tmp/kvmroot/etc/init.d ] && break
  sudo umount /tmp/kvmroot
done

echo "tailscale binary"
ls /tmp/kvmroot/usr/bin/tailscale* 2>/dev/null || echo "none"

echo "egress init scripts"
find /tmp/kvmroot/etc/init.d \
  \( -iname 'S*tailscale*' -o -iname 'S*ssdp*' \
     -o -iname 'S*dnsmasq*' -o -iname 'S*avahi*' \) -print

echo "tailscale"
sudo find /tmp/kvmroot -iname '*tailscale*' 2>/dev/null || echo "none"

sudo umount /tmp/kvmroot && sudo losetup -d "$LOOP"
```

This should come back empty, meaning the image is LAN-only.

## Flashing

You can either use Etcher to flash the image to an SD card, or manually via CLI:

```
lsblk   # find the entire disk, eg. /dev/sdb, not /dev/sdb1
sudo dd if="$IMG" of=/dev/sdX bs=4M conv=fsync status=progress
sync
```

## First boot

Insert the SD card, ethernet, USB-C into the host (both powers the KVM and allows it to act as a HID) and the HDMI and USB. Give it a minute on first boot while it expands the filesystem, then find its IP on your router and browse to it. Default credentials are `admin` / `admin`; once you log in it lets you set a new username and password.

![](/media/322c0d86-b8b3-4112-b9ff-ac211ea29ad7/426.webp)

NanoKVM defaults to WebRTC, which provides lower latency and bandwidth, at the cost of compression. As this device is LAN-only where bandwidth is not a constraint, you can switch the video mode to MJPEG for a sharper image.

![](/media/b78d7677-13c8-49a2-9ab2-db85b78efcc1/1203.webp)

## Notes

The image being LAN-only is not a substitute for network controls. You should still put the device on an isolated management VLAN with deny-by-default egress. The removed Tailscale binary and the firewall rule defend the same property from two different directions.
