# tuna ❤️ JetKVM

[JetKVM](https://jetkvm.com/) is a compact IP-KVM device based on the Rockchip RV1106 (ARM Cortex-A7). It allows you to remotely control a computer: see the screen, control the keyboard and mouse, mount disk images — all through a web interface in the browser.

By default, JetKVM is only accessible on the local network. With Tuna, you can expose it to the internet — without a public IP, without a VPN, and without port forwarding on the router.

![Tuna + JetKVM](/docs/img/tunnels/guides/jetkvm_tuna.png)

info

This guide is tested on JetKVM with system version **v0.2.7** or newer. Earlier versions do not support `/userdata/init.d/` — update JetKVM via the web interface (Settings → Device → Check for updates).

***

## Preparation

In addition to installation, the setup guide is split into 2 types depending on whether you have a paid Tuna subscription.

1. [On the free plan](#type-1-free-plan--http-tunnel)
2. [With a paid subscription](#type-2-subscription--ssh-and-http-tunnels)

### What you will need

* JetKVM with system version **v0.2.7 or newer**
* **Developer Mode** enabled in the JetKVM web interface (Settings → Advanced → Developer Mode)
* An **SSH key** added via the JetKVM web interface
* An account on [tuna.am](https://my.tuna.am/) (free or with a subscription)
* An auth token from the [token page](https://my.tuna.am/token)

### JetKVM system specifics

JetKVM runs on a custom embedded Linux (Buildroot + Rockchip SDK). Important differences from a regular Linux system:

| Feature              | Value                                              |
| -------------------- | -------------------------------------------------- |
| Shell                | `/bin/sh` (BusyBox ash). **bash is not available** |
| SSH server           | Dropbear (no scp, sftp, rsync)                     |
| Init system          | BusyBox init + scripts in `/userdata/init.d/`      |
| Persistent partition | `/userdata/` — preserved across OTA updates        |

Storage layout:

| Path         | Purpose   | Preserved across OTA |
| ------------ | --------- | -------------------- |
| `/`          | Root FS   | No                   |
| `/userdata/` | User data | **Yes**              |

We will store tuna in `/userdata/tuna/` so that the binary and configuration are preserved across updates.

### Connecting via SSH

Enable Developer Mode in the JetKVM web interface and add a public RSA key (Settings → Advanced → Developer Mode). Then connect:

```bash
ssh root@<JetKVM-IP-address>

```

Or open the terminal in the web interface:

![JetKVM terminal](/docs/img/tunnels/guides/jetkvm_terminal.png)

tip

You can find the IP address on the JetKVM built-in screen or in your router's DHCP table.

### Installing tuna on JetKVM

Connect to JetKVM via SSH and install the binary:

```bash
mkdir -p /userdata/tuna
INSECURE=true INSTALL_DIR=/userdata/tuna sh -c "$(wget -qO- http://releases.tuna.am/tuna/get.sh)"

```

Why `INSECURE=true`?

JetKVM does not have root CA certificates, so wget cannot verify the server's SSL certificate. The `INSECURE=true` flag switches the script to download over HTTP. This is safe because the tuna binary can be verified by checksum after download.

tip

The `/userdata` partition is preserved across OTA updates, so you will not need to reinstall tuna after firmware updates.

Save the auth [token](https://my.tuna.am/token):

```bash
/userdata/tuna/tuna --config=/userdata/tuna/.tuna.yml config save-token <YOUR_TOKEN>

```

tip

We recommend creating a dedicated token for the device.

***

## JetKVM auto-start system

Unlike NanoKVM, where auto-start is configured via `/etc/inittab` with the `respawn` directive, JetKVM uses init scripts in the `/userdata/init.d/` directory, which needs to be created.

```bash
mkdir -p /userdata/init.d

```

At boot, the JetKVM init system runs all scripts in `/userdata/init.d/` whose names match the pattern `S??*` (for example, `S99tuna-http`). Scripts are invoked with the `start` argument, and with the `stop` argument when stopping.

Critical rules for init scripts

Failing to follow these rules can cause the device to **fail to boot**:

1. **Use `#!/bin/sh`** — there is no `/bin/bash` on JetKVM. The `#!/bin/bash` shebang will cause an error and may block booting.
2. **Do not use the `.sh` extension** — files with the `.sh` extension are included via `source` (not invoked as a separate process). An `exit` command in such a script will terminate the entire init process.
3. **Do not block boot** — all long-running processes must be started **in the background** with `&`. If the script does not return control, boot will hang.
4. **Handle the `start` argument** — the init system invokes the script as `S99tuna-http start`. Without `case "$1"`, the script may not run correctly.

No automatic restart

Unlike NanoKVM (where `inittab respawn` automatically restarts a crashed process), on JetKVM `/etc/inittab` is overwritten during OTA updates. That's why we use a `while true` loop inside the init script to auto-restart on failure.

Test before rebooting

Always run the script manually (`/userdata/init.d/S99tuna-http start`) and make sure the prompt returns. If the console "hangs" after running it — the script will block boot on reboot.

***

## Type 1. Free plan — HTTP tunnel

The free plan provides an HTTP tunnel with a dynamic address. This is enough to expose the JetKVM web interface to the internet and control your computer through a browser.

Free plan limitations

* The tunnel address changes every **30 minutes**
* You cannot set a fixed subdomain
* The current address is always available in the [personal cabinet](https://my.tuna.am/tunnels)

### Creating the init script

```bash
cat << 'INITEOF' > /userdata/init.d/S99tuna-http
#!/bin/sh

PIDFILE=/tmp/tuna-http.pid
LOGFILE=/tmp/tuna-http.log

wait_network() {
    i=0
    while ! ip route | grep -q default; do
        sleep 1
        i=$((i + 1))
        if [ "$i" -ge 60 ]; then
            echo "$(date): network timeout" >> "$LOGFILE"
            return 1
        fi
    done
    return 0
}

start() {
    echo "$(date): starting tuna-http" >> "$LOGFILE"
    (
        wait_network || exit 1
        while true; do
            /userdata/tuna/tuna \
                --config=/userdata/tuna/.tuna.yml \
                --log="$LOGFILE" \
                http \
                --inspect=false \
                --basic-auth=tuna:SecurePasswd \
                --https-redirect \
                80 >> "$LOGFILE" 2>&1
            echo "$(date): tuna-http exited, restarting in 5s" >> "$LOGFILE"
            sleep 5
        done
    ) &
    echo $! > "$PIDFILE"
}

stop() {
    if [ -f "$PIDFILE" ]; then
        kill "$(cat "$PIDFILE")" 2>/dev/null
        pkill -f "/userdata/tuna/tuna.*http" 2>/dev/null
        rm -f "$PIDFILE"
    fi
}

case "$1" in
    start) start ;;
    stop) stop ;;
    restart) stop; sleep 1; start ;;
    *) echo "Usage: $0 {start|stop|restart}" ;;
esac
INITEOF
chmod +x /userdata/init.d/S99tuna-http

```

### Init script explanation

| Element                | Description                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `S99tuna-http`         | Name **without** `.sh` — init invokes the script with the `start` argument rather than including it via `source` |
| `wait_network()`       | Waits for a default route to appear (up to 60 seconds)                                                           |
| `( ... ) &`            | Subshell runs **in the background** — does not block boot                                                        |
| `while true`           | Auto-restarts tuna on failure (5 second pause)                                                                   |
| `echo $! > "$PIDFILE"` | Saves the subshell PID for proper stopping                                                                       |
| `pkill -f`             | On stop, kills both the wrapper and the child tuna process                                                       |

### tuna flag explanation

| Flag                              | Required | Description                                                            |
| --------------------------------- | -------- | ---------------------------------------------------------------------- |
| `--inspect=false`                 | No       | Disables the traffic inspector (not needed for KVM) to save RAM        |
| `--basic-auth=admin:YourPassword` | No       | Additional HTTP authentication ([details](#why-you-need---basic-auth)) |
| `--https-redirect`                | No       | Redirects HTTP to HTTPS                                                |
| `80`                              | Yes      | JetKVM web interface port                                              |

You can also [restrict access by IP subnets](#restricting-access-by-ip-subnets).

### Verification

Run the script manually:

```bash
/userdata/init.d/S99tuna-http start

```

Make sure the prompt returns (the script did not block the console). Check that the process is running:

```bash
ps | grep tuna

```

The output should contain a line with `/userdata/tuna/tuna ... http ...`. If there is no such process — see the [Troubleshooting](#troubleshooting) section.

In the logs, you will see your tunnel URL:

```bash
cat /tmp/tuna-http.log

```

The current address is also available in the [personal cabinet](https://my.tuna.am/).

***

## Type 2. Subscription — SSH and HTTP tunnels

With a [subscription](https://tuna.am/#pricing) you get:

* **Fixed subdomain** for the HTTP tunnel — the address does not change
* **SSH tunnel** — direct access to the JetKVM console from anywhere in the world

### HTTP tunnel with a fixed address

```bash
cat << 'INITEOF' > /userdata/init.d/S99tuna-http
#!/bin/sh

PIDFILE=/tmp/tuna-http.pid
LOGFILE=/tmp/tuna-http.log

wait_network() {
    i=0
    while ! ip route | grep -q default; do
        sleep 1
        i=$((i + 1))
        if [ "$i" -ge 60 ]; then
            echo "$(date): network timeout" >> "$LOGFILE"
            return 1
        fi
    done
    return 0
}

start() {
    echo "$(date): starting tuna-http" >> "$LOGFILE"
    (
        wait_network || exit 1
        while true; do
            /userdata/tuna/tuna \
                --config=/userdata/tuna/.tuna.yml \
                --log="$LOGFILE" \
                http \
                --subdomain=jetkvm \
                --inspect=false \
                --basic-auth=admin:YourPassword \
                --https-redirect \
                80 >> "$LOGFILE" 2>&1
            echo "$(date): tuna-http exited, restarting in 5s" >> "$LOGFILE"
            sleep 5
        done
    ) &
    echo $! > "$PIDFILE"
}

stop() {
    if [ -f "$PIDFILE" ]; then
        kill "$(cat "$PIDFILE")" 2>/dev/null
        pkill -f "/userdata/tuna/tuna.*http" 2>/dev/null
        rm -f "$PIDFILE"
    fi
}

case "$1" in
    start) start ;;
    stop) stop ;;
    restart) stop; sleep 1; start ;;
    *) echo "Usage: $0 {start|stop|restart}" ;;
esac
INITEOF
chmod +x /userdata/init.d/S99tuna-http

```

The difference from the free version is the `--subdomain=jetkvm` flag, which reserves a permanent address for you. After restarting the tunnel, the URL will remain the same.

info

The example subdomain may already be taken. Reserve your subdomain in advance in the [personal cabinet](https://my.tuna.am/domains), then use it in the `--subdomain=your-subdomain` flag.

#### HTTP tunnel flag explanation

| Flag                              | Required | Description                                                            |
| --------------------------------- | -------- | ---------------------------------------------------------------------- |
| `--subdomain=jetkvm`              | No       | Reserves a permanent tunnel address (subscription only)                |
| `--inspect=false`                 | No       | Disables the traffic inspector (not needed for KVM) to save RAM        |
| `--basic-auth=admin:YourPassword` | No       | Additional HTTP authentication ([details](#why-you-need---basic-auth)) |
| `--https-redirect`                | No       | Redirects HTTP to HTTPS                                                |
| `80`                              | Yes      | JetKVM web interface port                                              |

You can also [restrict access by IP subnets](#restricting-access-by-ip-subnets).

### SSH tunnel

The SSH tunnel allows you to connect to the JetKVM console over the internet. This is convenient for administration, firmware updates, and diagnostics.

```bash
cat << 'INITEOF' > /userdata/init.d/S98tuna-ssh
#!/bin/sh

PIDFILE=/tmp/tuna-ssh.pid
LOGFILE=/tmp/tuna-ssh.log

wait_network() {
    i=0
    while ! ip route | grep -q default; do
        sleep 1
        i=$((i + 1))
        if [ "$i" -ge 60 ]; then
            echo "$(date): network timeout" >> "$LOGFILE"
            return 1
        fi
    done
    return 0
}

start() {
    echo "$(date): starting tuna-ssh" >> "$LOGFILE"
    (
        wait_network || exit 1
        while true; do
            /userdata/tuna/tuna \
                --config=/userdata/tuna/.tuna.yml \
                --log="$LOGFILE" \
                ssh \
                --port=jetkvm-ssh \
                --record-session=false \
                --password-auth=false >> "$LOGFILE" 2>&1
            echo "$(date): tuna-ssh exited, restarting in 5s" >> "$LOGFILE"
            sleep 5
        done
    ) &
    echo $! > "$PIDFILE"
}

stop() {
    if [ -f "$PIDFILE" ]; then
        kill "$(cat "$PIDFILE")" 2>/dev/null
        pkill -f "/userdata/tuna/tuna.*ssh" 2>/dev/null
        rm -f "$PIDFILE"
    fi
}

case "$1" in
    start) start ;;
    stop) stop ;;
    restart) stop; sleep 1; start ;;
    *) echo "Usage: $0 {start|stop|restart}" ;;
esac
INITEOF
chmod +x /userdata/init.d/S98tuna-ssh

```

#### SSH tunnel flag explanation

| Flag                     | Required | Description                                                                                                                                                                                |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--port=jetkvm-ssh`      | No       | Reserves the SSH tunnel port. Without this flag, the port is assigned randomly on each startup. The [port alias](https://my.tuna.am/tcp_ports) must be reserved in advance before startup. |
| `--record-session=false` | No       | Disables recording SSH sessions to disk. Recommended on embedded devices with limited storage to save space.                                                                               |
| `--password-auth=false`  | No       | Disallows password authentication — access via SSH keys only ([details](#ssh-tunnel-authentication))                                                                                       |

You can also set [a static login and password](#ssh-tunnel-authentication) or [restrict access by IP subnets](#restricting-access-by-ip-subnets).

### Verifying tunnels

Run both scripts manually:

```bash
/userdata/init.d/S99tuna-http start
/userdata/init.d/S98tuna-ssh start

```

Make sure both processes are running:

```bash
ps | grep tuna

```

The output should contain two lines — one with `http` and one with `ssh`. If a process is missing — see the [Troubleshooting](#troubleshooting) section.

View the logs:

```bash
cat /tmp/tuna-http.log
cat /tmp/tuna-ssh.log

```

The HTTP tunnel logs will contain the URL, and the SSH tunnel logs will contain connection instructions with the address and port.

***

## Managing tunnels

Auto-restart on failure is provided by the `while true` loop inside the init script. Use the following commands to manage the tunnels.

### Check status

```bash
ps | grep tuna

```

### Stop a tunnel

```bash
# Stop the HTTP tunnel
/userdata/init.d/S99tuna-http stop

# Stop the SSH tunnel
/userdata/init.d/S98tuna-ssh stop

```

### Start a tunnel

```bash
# Start the HTTP tunnel
/userdata/init.d/S99tuna-http start

# Start the SSH tunnel
/userdata/init.d/S98tuna-ssh start

```

### Restart a tunnel

```bash
# Restart the HTTP tunnel
/userdata/init.d/S99tuna-http restart

# Restart the SSH tunnel
/userdata/init.d/S98tuna-ssh restart

```

### Fully disabling auto-start

To prevent a tunnel from starting at boot, rename the script removing the `S` prefix:

```bash
# Disable HTTP tunnel auto-start
mv /userdata/init.d/S99tuna-http /userdata/init.d/disabled-tuna-http

# Re-enable it
mv /userdata/init.d/disabled-tuna-http /userdata/init.d/S99tuna-http

```

***

## Recovery after an OTA update

The binary, configuration, and init scripts are stored in `/userdata/`, which is **preserved across OTA updates**. After a firmware update via the JetKVM web interface, the tunnels will keep working without any additional steps.

info

Unlike NanoKVM, on JetKVM you do not need to restore `/etc/inittab` after an update — auto-start via `/userdata/init.d/` fully survives OTA.

After a full re-flash (when `/userdata` was cleared), you will need to install the binary, save the token, and create the init scripts again.

***

## Additional security settings

### Why you need `--basic-auth`

JetKVM has its own authentication in the web interface. However, given the [history of vulnerabilities in IoT devices](https://owasp.org/www-project-internet-of-things/), we strongly recommend adding an extra layer of protection. When exposing access through the internet, it's better to be safe — the `--basic-auth` flag adds standard HTTP authentication (username and password in the browser) before the request even reaches JetKVM.

```bash
--basic-auth=mylogin:MyStr0ngP@ss

```

Important

Set **your own** login and password. Do not use the values from the examples.

### SSH tunnel authentication

**Option 1. SSH keys (recommended)**

The `--password-auth=false` flag disables password login. To connect, you will need to add a public SSH key in advance in the [personal cabinet](https://my.tuna.am/public_keys).

Detailed guide: [SSH keys setup](https://tuna.am/en/docs/guides/ssh-keys.md)

warning

Tunnel nodes have public addresses. Attackers can scan ports and start brute-forcing passwords. For long-lived SSH tunnels, we strongly recommend disabling password authentication.

**Option 2. Static login and password**

If SSH keys are not suitable, you can set fixed credentials:

```bash
--auth=user:pass

```

Replace `user` and `pass` with your own values.

### Restricting access by IP subnets

For HTTP and SSH tunnels, you can restrict access by IP addresses.

Allow access only from specific subnets:

```bash
--cidr-allow="203.0.113.0/24"

```

Deny access from specific subnets:

```bash
--cidr-deny="198.51.100.0/24"

```

You can combine — allow a subnet but exclude individual addresses:

```bash
--cidr-allow="10.0.0.0/24" --cidr-deny="10.0.0.33/32"

```

Caution

Use IP filtering carefully. If you make a mistake with the subnet or your IP address changes — you will lose remote access to JetKVM. Make sure you have an alternative way to manage the device (for example, local access).

***

## Troubleshooting

### Device fails to boot after adding an init script

Emergency recovery

If JetKVM stopped booting after adding a script to `/userdata/init.d/`, the most common causes are:

1. **`#!/bin/bash` shebang** — bash is not available on JetKVM
2. **Script blocks boot** — the process is not started in the background (`&`)
3. **`.sh` extension** — the script was included via `source`, which broke the init process
4. **Syntax error** — interrupts init execution

**Recovery methods:**

* **UART serial console** — if you have access to the JetKVM UART port, connect via serial console (115200 baud) and remove or rename the problematic script
* **Factory reset** — press and hold the reset button on JetKVM (note: this may clear `/userdata/`)

### tuna process does not start

Check that the process is in the list:

```bash
ps | grep tuna

```

If the output has no lines with `/userdata/tuna/tuna`, check:

1. The script exists and is executable:

```bash
ls -la /userdata/init.d/S9*tuna*

```

2. The script runs without errors:

```bash
/userdata/init.d/S99tuna-http start

```

3. The tuna binary works:

```bash
/userdata/tuna/tuna version

```

### Viewing logs

Tunnel logs are written to `/tmp/` and are available until the device reboots:

```bash
# HTTP tunnel logs
cat /tmp/tuna-http.log

# SSH tunnel logs
cat /tmp/tuna-ssh.log

```

To watch logs in real time:

```bash
tail -f /tmp/tuna-http.log

```

### Common errors in logs

| Error                     | Cause                              | Solution                                                                                |
| ------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
| `token is invalid`        | Invalid or expired token           | Repeat `config save-token` with a fresh token from the [page](https://my.tuna.am/token) |
| `Domain already reserved` | Subdomain is taken by another user | Choose another name via `--subdomain=`                                                  |
| `port alias not found`    | Port alias is not reserved         | Reserve the alias in the [personal cabinet](https://my.tuna.am/tcp_ports)               |
| `connection refused`      | No internet access                 | Check the network: `ping -c 3 8.8.8.8`                                                  |
| `exec format error`       | Wrong binary architecture          | Make sure you are using `tuna_linux_arm` (ARMv7)                                        |

### No internet access

Check that JetKVM has a network connection:

```bash
# Check for an IP address
ip addr

# Check the default route
ip route

# Check internet access
ping -c 3 8.8.8.8

```

### Logs disappeared after a reboot

Logs are stored in `/tmp/`, which is cleared on reboot. This is normal behavior. If you need to keep logs across reboots, change the path in the init scripts:

```bash
LOGFILE=/userdata/tuna/tuna-http.log

```

warning

Frequent log writes may shorten the storage's lifespan. Use this only for temporary debugging.

### Tunnel was working and stopped

1. Check the logs for errors
2. Make sure the token is still valid in the [personal cabinet](https://my.tuna.am/token)
3. Check that the script in `/userdata/init.d/` is in place and executable:

```bash
ls -la /userdata/init.d/S9*tuna*

```

***

## Links

* [JetKVM — official site](https://jetkvm.com/)
* [JetKVM — GitHub](https://github.com/jetkvm/kvm)
* [JetKVM — documentation](https://jetkvm.com/docs)
* [HTTP tunnel — all flags](https://tuna.am/en/docs/tunnels/http.md)
* [SSH tunnel — all flags](https://tuna.am/en/docs/tunnels/ssh.md)
* [SSH keys setup](https://tuna.am/en/docs/guides/ssh-keys.md)
* [Running tuna as a service](https://tuna.am/en/docs/tunnels/guides/service.md)

***

## Availability monitoring

In addition to tunnels for remote access, you can set up availability monitoring for JetKVM. Heartbeat monitoring will send you notifications if the device loses internet connectivity or stops responding.

**Full guide:** [JetKVM availability monitoring](https://tuna.am/en/docs/monitors/examples/jetkvm.md)
