安全 YOLO 模式:在 Libvirt 和 Virsh 中运行 LLM 代理虚拟机
Safe YOLO Mode: Running LLM agents in vms with Libvirt and Virsh

原始链接: https://www.metachris.dev/2026/02/safe-yolo-mode-running-llm-agents-in-vms-with-libvirt-and-virsh/

## 使用 Libvirt & Virsh 隔离 LLM 代理 本指南详细介绍了如何在 Linux 服务器上使用 libvirt 和 virsh 在虚拟机 (VM) 中隔离大型语言模型 (LLM) 代理。VM 隔离可以增强安全性,保护您的主机系统免受潜在的 LLM 操作(如未经授权的文件访问)的侵害——尤其是在授予代理广泛权限(“yolo 模式”)时,这一点至关重要。它还方便长时间运行的会话和远程访问。 Libvirt 提供了一个统一的 API,用于跨各种虚拟机管理程序管理 VM,使其非常适合生产环境。安装涉及安装软件包 (`qemu-kvm`, `libvirt-daemon-system`, `virtinst`) 并启动 libvirt 守护进程。建议使用 Ubuntu 云镜像,以便使用 cloud-init 进行快速、可脚本化的配置。 该过程包括下载云镜像、调整其磁盘空间以及使用 `virt-install` 创建 VM。然后可以通过 SSH(使用 VM 的内部 IP 或 Tailscale 等工具进行远程访问)或 virsh 控制台进行访问。基本的 VM 管理命令包括启动、停止、列出和拍摄快照。 可以通过 cloud-init 配置进行自定义,并且可以在 VM 中安装 Claude Code、Gemini CLI 和 Codex CLI 等工具。对于暴露服务,建议使用 Cloudflare Tunnel 或 Tailscale Funnel 等隧道。此设置为开发和运行 LLM 代理提供了一个强大而安全的环境。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 安全 YOLO 模式:使用 Libvirt 和 Virsh 在虚拟机中运行 LLM 代理 (metachris.dev) 6 分,by metachris 1 小时前 | 隐藏 | 过去 | 收藏 | 2 评论 dk8996 1 分钟前 | 下一个 [–] 有趣。我正在寻找在云端运行多个 OpenClaw 机器人的解决方案,并考虑到安全性和隔离性。回复 KaiserPro 14 分钟前 | 上一个 [–] 虽然在虚拟机/容器内运行更安全,但这并不意味着它是安全的。是的,现在整个文件系统被删除的可能性要小得多(为每个操作的镜像拍摄 zfs 快照可以获得额外积分),但你的上下文仍然容易受到攻击,因为虚拟机可以访问任何内容。回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

This is a guide for isolating LLM agents in virtual machines, using libvirt and virsh on Linux servers.

Running LLMs in VMs isolates them from the host system, mitigating numerous security risks such as destructive operations or unauthorized file access (i.e. private keys, secrets, credentials for communication tools). This is particularly important when granting LLM agents broad permissions, like auto-approving tool use (“yolo mode”). It’s also useful to keep sessions running for extended periods of time, and to interact with agents from the phone / on the go.

Related content: I published Sandbox Your AI Dev Tools: A Practical Guide for VMs and Lima back in November, which uses Lima VM for macOS/Linux desktop use. Another noteworthy related post is Claude Code On-The-Go (granda.org) which concisely outlines a neat remote Claude Code setup, where I drew some inspiration from.



Why Libvirt and Virsh?

Libvirt is the standard virtualization API for Linux, providing a unified interface to manage VMs across different hypervisors (KVM, QEMU, Xen, etc.). The virsh command-line tool is the primary way to interact with libvirt.

Libvirt is ideal for production-grade VM isolation of LLM agents on Linux servers, and the combination of Ubuntu cloud images and cloud-init makes VM provisioning fast, pleasant, and scriptable.

Libvirt vs Lima: When to use which?

Both libvirt/virsh and Lima are excellent tools for VM-based isolation, with some notable differences:

AspectLibvirt/VirshLima
Best forLinux serversmacOS, Linux desktop
Production useCommon, battle-testedPrimarily for development
Hypervisor supportKVM/QEMU, Xen, LXC, etc.Apple’s Virtualization.framework, QEMU
Resource overheadLowerSlightly higher
Setup complexitySimple (apt install)Simple (brew install lima)
Host directory sharingManual (9p, virtiofs)Built-in, YAML config, home by default (dangerous)
Port forwardingManual iptables/NAT configBuilt-in, YAML config
GUI toolsvirt-manager availableNone (CLI only)
SnapshotsNative, robustNot working on macOS

For server-based LLM agent isolation, libvirt is generally the better choice due to its maturity, lower overhead, and robust management features.


Installation

Install the required packages on your Ubuntu/Debian server:

sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system virtinst

Enable and start the libvirt daemon:

sudo systemctl enable --now libvirtd

Verify the installation:

To avoid needing sudo, you could add your user to the libvirt group (requires re-login):

sudo usermod -aG libvirt $USER

Download a Cloud Image

We’ll use Ubuntu, which provides pre-built cloud images. These images boot quickly and work seamlessly with cloud-init for automated provisioning. Download an image to /var/lib/libvirt/images/project1-ubuntu.img:

wget -O /var/lib/libvirt/images/project1-ubuntu.img https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

Notes:

  • For ARM servers, use the `arm64` variant instead
  • You can find all available OS variants with osinfo-query (click to expand)
    sudo apt install libosinfo-bin
    osinfo-query os | grep ubuntu
    

Increase the disk size of the image to 40 GB (or more):

sudo qemu-img resize /var/lib/libvirt/images/project1-ubuntu.img 40G

Create a VM

Use virt-install to create a new VM with cloud-init for automatic provisioning:

sudo virt-install \
  --name project1 \
  --ram 16384 \
  --vcpus 4 \
  --import \
  --disk /var/lib/libvirt/images/project1-ubuntu.img \
  --os-variant ubuntu24.04 \
  --cloud-init

This creates a VM named project1 with:

  • Ubuntu 24.04 as the guest OS
  • 16 GB RAM
  • 4 vCPUs
  • 40 GB disk (defined by resizing the image earlier)
  • Cloud-init for automatic setup

The VM starts within a few seconds and you’ll be in the console. You can exit it with Ctrl + ].


Access the VM

The Linux console (which you can open with virsh console project1 --force) provides a pretty rudimentary way to interact with the VM. Use SSH for a better experience!

Either use the internal IP address assigned to the VM, or set up Tailscale for easy remote access. First, add your SSH key to the VM - add it to /home/ubuntu/.ssh/authorized_keys.

vim /home/ubuntu/.ssh/authorized_keys

SSH using the internal IP

Libvirt sets up a default NAT network that provides VMs with internal IP addresses and internet access:

# Get the VM's IP address
virsh domifaddr project1

 Name       MAC address          Protocol     Address
-------------------------------------------------------
 vnet0      52:54:00:xx:xx:xx    ipv4         192.168.122.xxx/24

# SSH into the VM
ssh [email protected]

To access the VM from a remote machine, use the host as a jump server with ProxyJump. Note that cloud-init only adds the host’s SSH key by default, so you’ll need to add your remote machine’s public key to the VM first:

# From any remote machine (one-liner)
ssh -J user@your-host [email protected]

# Or configure in ~/.ssh/config for convenience
Host project1-vm
    HostName 192.168.122.xxx
    User ubuntu
    ProxyJump user@your-host

Tailscale for remote access

Tailscale creates an encrypted mesh VPN between your devices, letting you connect directly to the VM from anywhere on your tailnet, without exposing anything to the public internet. It also works when the host is behind NAT or a firewall without port forwarding. Once installed, the VM gets a stable IP and hostname on your private tailnet.

# Install Tailscale inside the VM
curl -fsSL https://tailscale.com/install.sh | sh

# Connect to your tailnet
sudo tailscale up

# Get your Tailscale IP
tailscale ip -4
100.95.xxx.xxx

Use this Tailscale IP (or the Tailscale hostname) to access the VM from any machine on your tailnet:


VM Setup

Basic tools and configuration

Once inside the VM, do some basic setup:

# Update system and install essentials
sudo apt update && sudo apt upgrade -y
sudo apt install -y vim git curl build-essential htop jq ca-certificates pkg-config libssl-dev
sudo timedatectl set-timezone UTC

# Configure git
git config --global user.name "Your Name"
git config --global user.email "[email protected]"

Tmux for persistent sessions

For a persistent coding experience, I like the ubuntu user to auto-resume the last tmux session on login. To achieve this, I add a little helper to ~/.bashrc:

tee -a ~/.bashrc > /dev/null << 'EOF'

# Open tmux session, if not already inside
if [[ -z "$TMUX" && $- == *i* && -t 0 ]]; then
	tmux attach -t main 2>/dev/null || tmux new -s main
fi

EOF

Let’s source the updated ~/.bashrc to apply the changes immediately and jump into tmux:

Bash utilities and helpers

💡 I like to add a few opinionated goodies to /etc/bash.bashrc (click to expand).
sudo tee -a /etc/bash.bashrc > /dev/null << 'EOF'

# Long bash history!
export HISTSIZE=262144
export HISTFILESIZE=262144

# Path with Go, default editor, and disable email notifications
export PATH=$PATH:/usr/local/go/bin:~/go/bin/:~/.local/bin
export EDITOR="vim"
unset MAILCHECK

# General aliases
alias ll='ls -alh'
alias ai="sudo apt-get install -y"
alias as="apt search"
alias htop="htop --sort-key=PERCENT_CPU"
alias v="git describe --tags --always --dirty=-dev"
alias s="systemctl status"
alias j="journalctl -o cat"

#
# Git
#
alias g="git"
alias gs='git status -sb'
alias gd="git diff"
alias ga='git add'
alias gb='git branch'
alias gc='git commit'
alias gl='git log --pretty=format:"%h %ad | %s%d [%an]" --graph --date=short'
alias ggo="git checkout"
alias gds='git diff --staged'
alias gca="git commit -a --amend"
alias gcap="git commit -a --amend --reuse-message=HEAD && gitpbf"

# Push current branch to origin
function gitpb ()
{
    branch=$( git rev-parse --abbrev-ref HEAD );
    cmd="git push origin $branch";
    echo $cmd;
    $cmd
    git push --tags
}

# Force push current branch to origin (use with caution!)
function gitpbf ()
{
    branch=$( git rev-parse --abbrev-ref HEAD );
    cmd="git push origin $branch --no-verify --force";
    echo $cmd;
    $cmd
}

# Set upstream for current branch to origin (useful after creating a new branch)
function git-set-upstream ()
{
    branch=$( git rev-parse --abbrev-ref HEAD );
    cmd="git branch --set-upstream-to=origin/$branch $branch";
    echo $cmd;
    $cmd
}

#
# TMUX
#
alias t='tmux'
alias ta='tmux attach'
alias tl='tmux list-sessions'
alias td='tmux detach'
alias tks='tmux kill-server'

# Attach to session (with optional name): tt [name]
# If no name given, attaches to last session or creates new one
function tt() {
    if [ -n "$1" ]; then
        tmux attach-session -t "$1" 2>/dev/null || tmux new-session -s "$1"
    else
        tmux attach 2>/dev/null || tmux new-session
    fi
}

# New session (with optional name): tn [name]
function tn() {
    if [ -n "$1" ]; then
        tmux new-session -s "$1"
    else
        tmux new-session
    fi
}

# Kill session by name: tk <name>
function tk() {
    if [ -n "$1" ]; then
        tmux kill-session -t "$1"
    else
        echo "Usage: tk <session-name>"
        tl
    fi
}

EOF

Tmux keybindings for H / J / K / L to resize a pane:

tee -a ~/.tmux.conf > /dev/null << 'EOF'

bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5

EOF

Installing basic tools

For detailed instructions on installing Claude Code, Gemini CLI and Codex CLI, see the tool installation section in the Lima guide; the steps are identical once you’re inside the VM.

Node.js

# Install nvm
NVM_LATEST=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | jq -r .tag_name)
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_LATEST}/install.sh" | bash
source ~/.bashrc

# Install Node.js LTS
nvm install --lts

fzf (fuzzy finder + bash fuzzy search)

FZF_LATEST=$(curl -s https://api.github.com/repos/junegunn/fzf/releases/latest | jq -r .tag_name)
curl -Lo fzf.tar.gz "https://github.com/junegunn/fzf/releases/download/${FZF_LATEST}/fzf-${FZF_LATEST#v}-linux_amd64.tar.gz"
sudo tar -xzf fzf.tar.gz -C /usr/local/bin fzf
rm fzf.tar.gz
echo 'eval "$(fzf --bash)"' >> ~/.bashrc

Golang

# Download latest release
GO_LATEST=$(curl -s 'https://go.dev/dl/?mode=json' | jq -r '.[0].files[] | select(.os=="linux" and .arch=="amd64") | .filename')
curl -L "https://go.dev/dl/$GO_LATEST" -o go.tar.gz

# Extract and cleanup
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go.tar.gz
rm go.tar.gz

Docker / Containerd + nerdctl

If you want Docker, consider containerd and nerdctl. It’s pretty much fully Docker-compatible and works well in VMs.

# Download and install nerdctl (full package includes containerd, CNI plugins and BuildKit)
NERDCTL_LATEST=$(curl -s https://api.github.com/repos/containerd/nerdctl/releases/latest | jq -r .tag_name)
curl -sSL "https://github.com/containerd/nerdctl/releases/download/${NERDCTL_LATEST}/nerdctl-full-${NERDCTL_LATEST#v}-linux-amd64.tar.gz" | sudo tar -xz -C /usr/local

# Enable and start containerd, and BuildKit for image building
sudo systemctl enable --now containerd
sudo systemctl enable --now buildkit

Test the installation

sudo nerdctl ps
sudo nerdctl run --rm hello-world

Install LLMs

Claude Code

curl -fsSL https://claude.ai/install.sh | bash
echo 'alias claude="claude --dangerously-skip-permissions"' >> ~/.bashrc

Gemini

npm install -g @google/gemini-cli@latest
echo 'alias gemini="gemini --yolo"' >> ~/.bashrc

Codex CLI

npm install -g @openai/codex@latest
echo 'alias codex="codex --dangerously-bypass-approvals-and-sandbox"' >> ~/.bashrc

Expose Services with a Tunnel

To make a dev server - or any service - accessible via the public internet (e.g., for webhooks, demos, or API testing), you can use tunnels such as Cloudflare Tunnel, ngrok or Tailscale Funnel.

For using Cloudflare, install cloudflared inside the VM:

curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

Create a quick tunnel (no account required, temporary URL):

cloudflared tunnel --url http://localhost:3000

For persistent tunnels, authenticate first:

cloudflared tunnel login
cloudflared tunnel create my-tunnel
cloudflared tunnel route dns my-tunnel myapp.example.com
cloudflared tunnel run my-tunnel

VM Management with Virsh

Here are the essential commands for managing your VMs:

Lifecycle commands

# List all VMs (running and stopped)
virsh list --all

# Start a VM
virsh start project1

# Enable autostart (start on boot)
virsh autostart project1

# Disable autostart
virsh autostart --disable project1

# Gracefully shutdown a VM
virsh shutdown project1

# Force stop a VM (like pulling the power)
virsh destroy project1

# Delete a VM and its storage
virsh undefine project1 --remove-all-storage

# Reboot a VM
virsh reboot project1

Console access

# Connect to the VM console
virsh console project1

# If an existing session is active, you may need to force it:
virsh console project1 --force

Press Ctrl + ] to detach from the console.

VM information

# Show VM details
virsh dominfo project1

# Show VM IP address
virsh domifaddr project1

# Show VM disk information
virsh domblklist project1

Customizing Cloud-Init

For more control over VM provisioning, create a custom cloud-init configuration.

Create a user-data.yaml file:

#cloud-config
users:
  - name: dev
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... your-key-here

packages:
  - vim
  - htop
  - git
  - build-essential

runcmd:
  - echo "VM provisioned at $(date)" > /var/log/provision.log

Create the VM with your custom config:

sudo virt-install \
  --name project1 \
  --ram 16384 \
  --vcpus 4 \
  --import \
  --disk /var/lib/libvirt/images/project1-ubuntu.img \
  --os-variant ubuntu24.04 \
  --cloud-init user-data=user-data.yaml

Snapshots

Snapshots let you save the VM state and revert if something goes wrong; useful before running experimental LLM-generated code.

# Shutdown the source VM first
virsh shutdown project1

# Create a snapshot (takes about 10 seconds)
virsh snapshot-create-as project1 --name "before-experiment" --description "Clean state"

# List snapshots
virsh snapshot-list project1

# Revert to a snapshot
virsh snapshot-revert project1 --snapshotname "before-experiment"

# Delete a snapshot
virsh snapshot-delete project1 --snapshotname "before-experiment"

Cloning VMs

Clone an existing VM to quickly spin up new instances:

# Shutdown the source VM first
virsh shutdown project1

# Clone the VM
virt-clone --original project1 --name project12 --auto-clone

# Start the clone
virsh start project12

Network Configuration

By default, libvirt creates a NAT network (default) that provides VMs with internet access and internal IPs.

# List networks
virsh net-list --all

# Show network details
virsh net-info default

# Show DHCP leases
virsh net-dhcp-leases default

For production setups, consider using bridged networking to give VMs direct access to your network.


Quick Reference

# Installation
sudo apt install qemu-kvm libvirt-daemon-system virtinst
sudo systemctl enable --now libvirtd

# Download cloud image
wget -O /var/lib/libvirt/images/project1-ubuntu.img https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

# Create VM
sudo virt-install --name project1 --ram 16384 --vcpus 4 \
  --import --disk /var/lib/libvirt/images/project1-ubuntu.img \
  --os-variant ubuntu24.04 --cloud-init

# VM lifecycle
virsh list --all             # List VMs
virsh start project1         # Start
virsh autostart project1     # Enable autostart
virsh console project1       # Console access (Ctrl+] to exit)

virsh shutdown project1      # Graceful shutdown
virsh destroy project1       # Force stop
virsh undefine project1 --remove-all-storage

# Snapshots
virsh snapshot-create-as project1 --name "clean"
virsh snapshot-revert project1 --snapshotname "clean"

# Cloning
virt-clone --original project1 --name project12 --auto-clone

I hope this guide is useful to you! Questions and feedback welcome in the comments below.

联系我们 contact @ memedata.com