This is the backend server implementation for NanoKVM.
For detailed documentation, please visit our Wiki.
server
├── common // Common utility components
├── config // Server configuration
├── dl_lib // Shared object libraries
├── include // Header files for shared objects
├── logger // Logging system
├── middleware // Server middleware components
├── proto // API request/response definitions
├── router // API route handlers
├── service // Core service implementations
├── utils // Utility functions
└── main.goThe configuration file path is /etc/kvm/server.yaml.
# Network Settings
proto: http # Access protocol. Can be changed to `https` only when certificates are configured. Default is `http`
host: "" # The listening address for the HTTP/HTTPS service. If left empty, all network interfaces will be bound
port:
http: 80 # The listening port for the HTTP service. Default is `80`
https: 443 # The listening port for the HTTPS service (effective when HTTPS is enabled). Default is `443`
cert:
crt: server.crt # The path to the public key certificate for HTTPS
key: server.key # The path to the private key file for HTTPS
# Logging Configuration
logger:
level: info # Global log output level. Evaluated options from highest to lowest detail: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. Default is `info`
file: stdout # Log output destination. `stdout` outputs to the standard console. A file path directs log output to that file. Default is `stdout`
# Authentication & Security
authentication: enable # Whether to enable identity verification for HTTP API and Web endpoints. Options are `enable` or `disable`. Default is `enable`. Highly recommended to leave this enabled for internet-facing devices!
jwt:
secretKey: "" # The secret key used to sign and verify JWT Tokens. If left empty, a random key will be generated automatically on startup
refreshTokenDuration: 2678400 # The token refresh duration threshold in seconds before forcing a re-login. Default is `2678400` (~31 days)
revokeTokensOnLogout: true # Whether logout invalidates all sessions belonging to that user. Other users are never logged out. Setting this to false only clears the browser cookie and is not recommended. Default is `true`
security:
loginLockoutDuration: 0, # The duration (in seconds) to ban an IP from attempting to log in again after reaching the failure limit. If set to `0` or left empty, brute-force protection is disabled. Default is `0`
loginMaxFailures: 5, # The maximum number of continuous failed login attempts allowed per IP before triggering protection. Default is `5`
# WebRTC Traversal Settings
stun: stun.l.google.com:19302 # The default STUN server address used for NAT hole-punching to establish P2P streams
turn:
turnAddr: example_addr # The relay (TURN) server address (format `ip:port`) used as a fallback when P2P connection fails. Leave empty to disable TURN relay
turnUser: example_user # The username required for authorization to the TURN server
turnCred: example_cred # The credential/password required for authorization to the TURN serverNanoKVM uses two device-wide roles:
admin: everything below, plus user management.user: only what the account is granted.
A grant names an action on one party, and the catalog the server serves to the account editor carries its group, its risk, and the grants it implies:
| Group | Grants |
|---|---|
| console (this session) | console.view, console.control |
| target machine | power, medium.attach |
| media library (this device) | library.read, library.write |
| this device | terminal, device, network, update |
| services | mcp, tailscale, ai |
Implied grants are declared once, in the catalog, and the server resolves them before answering any
request: console.control implies console.view, both medium.attach and library.write imply
library.read, and every grant whose controls live in the console menu — power, device,
network, update, mcp, tailscale and ai — implies console.view too, because no page
outside the console renders them. Consequences worth knowing:
- An account can hold no console access at all. The web app then lands on a page that offers the
sections the account may use (the media library, the terminal) instead of a console it cannot
open.
library.readis all that media access needs, because the other two media grants imply it, and onlyterminaland the media grants can stand on their own pages in the first place. - The media grants split along the trust boundary.
medium.attachmounts or ejects a medium on the target machine and writes into the shared partition the target reads;library.writechanges what the library keeps (uploads, deletes, the files inside an image);library.readlists and downloads. The file endpoints serve an image and the shared partition through one path, so the route admits either grant and each request is checked against the medium type it names. Every.isoand.imgon the partition is a library entry, so uploading one through the shared partition, or deleting a file or folder that holds one, also needslibrary.write. - The emulated USB devices are neither media nor network: the virtual disk and the RNDIS adapter
hand the target machine the raw data partition and a network interface, so both follow
device. A medium can only be attached while adeviceholder has the virtual disk switched on, and the storage page says so when it is off.
Four grants reach further than their names suggest and amount to full device control. The catalog
has two risk levels, and warn is reserved for them: the account editor marks those four and
names them before the grants are saved:
terminalis a root shell (the same one SSH gives) and can run saved and autostart scripts.mcphands out a device-wide API key that drives the keyboard, mouse and screenshots from outside the web session. The key is shared, so a copy keeps working after an account loses this grant. Whenever a change takes the grant away (revoked, demoted, disabled or deleted), the account editor warns and offers to regenerate the key, checked by default; regenerating disconnects every MCP client until it is given the new key, and an administrator who keeps the key accepts that a copy stays valid. The API takes the choice asrotateMcpKey(a body field on update, a query parameter on delete) and never rotates without it.aidrives the device through the local loopback interface, which the other grants do not restrict.updateinstalls firmware and application packages. An offline package is checked only against the checksum its uploader supplies, so installing one is running code as root.
device carries no such mark, although it changes what the device does for everyone and cannot be
narrowed per account. device turns SSH on or off (root login, with the device
owner's password), reboots the device, hands the raw data partition or a network interface to the
target machine through the emulated USB devices, and edits the OLED, HDMI, mDNS, memory, swap and
HID settings. The editor describes it that way.
Two grants also change settings that affect everyone watching: console.view sets the video mode,
resolution, frame rate and quality, and resets the HDMI capture; power manages the Wake-on-LAN
device list. Both are described that way in the editor.
Administrators manage accounts from Settings > Account. Account data remains in
/etc/kvm/pwd; the server migrates the legacy single-account JSON format in place and writes
the multi-user format atomically with mode 0600. Keeping the same path preserves the physical
BOOT-button password reset behavior.
Users must confirm their current password when changing it themselves; administrators can reset
non-owner users from the account manager. Only the device owner can change the device owner's
password, because that password is also synchronized to the Linux root account.
All authenticated sessions are backed by the current account state: every request is authorized
against the account file, so a revoked grant stops working immediately without re-issuing a login.
Disabling, deleting, or resetting the password of a user invalidates that user's HTTP and real-time
connections without affecting other users. Changing the role or the grants of an account keeps its
login valid: the account is told to refresh, and only the long-lived connections whose cached
state depends on what changed are dropped (console, video stream, terminal, AI), so those clients
reconnect under the new grants. An account that loses console.view is sent to the landing page
instead of a console it can no longer open. Multiple
users may watch and control the KVM concurrently; input uses the existing cooperative HID
coordinator, so simultaneous input can interleave. A user without console.control keeps the
video and the menu for its other grants, but the console rejects keyboard and mouse reports and
the web UI marks the session as view only.
Video mode, quality, resolution, MJPEG frame-detection controls, and resetting the HDMI capture
require only console.view and stay shared KVM operations; when several users adjust them
concurrently, the latest change applies device-wide. Enabling or disabling the HDMI capture and
its idle timeout are device settings. console.control is input only: keyboard, mouse, paste, and
the input region that maps pointer motion onto the target screen.
The account file stores an explicit grant set: a record written before fine-grained permissions
keeps exactly what the user role used to grant, and the split media grants are translated on load,
so the store never holds a missing set that would fall back to different behavior later. Grants
this build does not know are preserved and ignored, so a downgrade cannot lock the device out.
Creating a regular account without naming a grant set keeps that same meaning, so a script that only
knows about roles gets the account the user role used to create; the account editor seeds a new
regular account with console.view only, and an explicitly empty set really is empty.
Note: The manual steps below require a Linux x86-64 host with Go 1.25 or newer; they are not compatible with ARM, Windows or macOS. With Docker you can skip them entirely and use the containerized flow instead — the root Makefile (make shell) or the dev container (see "Development" in the root README) — which works on any host OS; run server/build.sh inside the container for a release-equivalent build.
-
Install the Toolchain
- Download the toolchain from the following link: Download Link.
- Extract the file and add the
host-tools/gcc/riscv64-linux-musl-x86_64/bindirectory to your PATH environment variable. - Run
riscv64-unknown-linux-musl-gcc -v. If there is version information in the output, the installation is successful.
-
Compile the Project
- Run
cd serverfrom the project root directory. - Run
go mod tidyto install Go dependencies. - (Optional) If you compiled
libkvm.soyourself, you need to modify its RPATH bypatchelf --add-rpath \$ORIGIN ./dl_lib/libkvm.so. - Run
CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go buildto compile the project. - After compilation, an executable file named
NanoKVM-Serverwill be generated.
- Run
-
Modify RPATH
- Run
sudo apt install patchelforpip install patchelfto install patchelf. - Run
patchelf --version. Ensure the version is 0.14 or higher`. - Run
patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Serverto modify the RPATH of the executable file.
- Run
-
Deploy the Application
- File uploads requires SSH. Please enable it in the Web Settings:
Settings > SSH; - Replace the original file in the NanoKVM
/kvmapp/server/directory with the newly compiledNanoKVM-Server. - Restart the service on NanoKVM by executing
/etc/init.d/S95nanokvm restart.
- File uploads requires SSH. Please enable it in the Web Settings:
File uploads requires SSH. Please enable it in the Web Settings:
Settings > SSH;
- Download the latest application from GitHub;
- Unzip the downloaded file and rename the unzipped folder to
kvmapp; - Back up the existing
/kvmappdirectory on your NanoKVM, then replace it with the newkvmappfolder; - Run
/etc/init.d/S95nanokvm restarton your NanoKVM to restart the service.