May 22, 2026 9 min read

Securing a Virtual Machine for use with Claude

Securing a Virtual Machine for use with Claude
Table of Contents
💻
I'm going to be doing a lot more testing with Claude locally (Claude Code and the Claude API) and I have been concerned about security. My plan is to do all testing in a Debian 13 virtual machine that runs on KVM.
So, I spent the morning writing up this security document and testing it on the VM. It's done, the system is much more secure, and now I can get to work on my testing. Check it out!

VM Security for Claude Use

You're about to give an AI access to your terminal, your filesystem, and your API keys — what could possibly go wrong? Quite a lot, actually, which is exactly why this guide exists. Securing your VM properly means Claude Code gets to do its job while attackers, malicious prompts, and rogue MCP servers do not. Think of this as putting a very good lock on the door before handing the key to a very capable but occasionally overzealous assistant. Security is one of the most underserved topics in the Claude ecosystem right now — so consider this document a head start.


Example Environment

Component Details
VM Debian — 10 CPU cores, 32 GB RAM, 30 GB Storage
Host Ubuntu Linux / KVM

1. KVM Host Layer (Ubuntu)

These apply to the Ubuntu host, not the VM:

  • Keep QEMU/KVM packages updated — update them individually with sudo apt install --only-upgrade <package>, or do a full system update with sudo apt update && sudo apt upgrade
    • qemu-kvm — QEMU hypervisor with KVM acceleration
    • qemu-system — full system emulation support
    • qemu-utils — QEMU disk image utilities (qemu-img, etc.)
    • libvirt-daemon-system — libvirt daemon and default configuration
    • libvirt-clients — virsh and other libvirt client tools
    • libguestfs-tools — tools for manipulating VM disk images
    • bridge-utils — network bridge management
    • virtinst — virt-install command for creating VMs
    • virt-manager — graphical VM management interface
  • Restrict access to libvirt — only your user should be in the libvirt group
    • Check this with the getent group libvirt command.
  • Do not expose the KVM management socket or QEMU monitor to the network — both are local Unix sockets by default and not network-exposed, but verify with these commands:
    • Check if libvirtd is listening on any TCP port: ss -tlnp | grep libvirt
    • Check if any QEMU process is exposing a TCP monitor: ss -tlnp | grep qemu
    • Check libvirt daemon config for TCP listener: grep -i listen /etc/libvirt/libvirtd.conf
    • If the ss commands return nothing and libvirtd.conf shows listen_tcp = 0 or the listen lines are commented out, you are not exposed.
  • AppArmor is already active on Ubuntu and has a KVM/QEMU profile by default — verify with sudo aa-status. In the output, look for libvirtd, virt-aa-helper, and your VM's generated profile (e.g. libvirt-48edf7df-568f-4d74-a40a-fa7374cfb867) all listed under enforce mode.
  • Use Spice (not VNC) for VM console access via Virtual Machine Manager — Spice is the preferred display protocol for KVM/libvirt, is faster than VNC, and binds to 127.0.0.1 by default, meaning it is local-only and not network-exposed. Verify this in Virtual Machine Manager under the VM's display settings.

2. Debian VM — OS Hardening

Note: From this section onward, all steps apply to the Debian VM, not the Ubuntu host.

Warning: Check for any remote access programs in your VM and shut them down. Software such as NoMachine, TeamViewer, AnyDesk, or VNC servers can silently open ports and expose your VM to the network. Remove anything you don't explicitly need.

Install Updates:

sudo apt update && sudo apt upgrade

Minimal install — disable unnecessary services and daemons to reduce attack surface. Use the command systemctl list-unit-files to see what services are enabled.

Install and enable automatic security updates:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

Enter Yes when prompted.

Restrict SSH — disable password auth, disable root login, use keys only.
Add to /etc/ssh/sshd_config.d/00-hardening.conf:

PasswordAuthentication no
PermitRootLogin no
PermitEmptyPasswords no
MaxAuthTries 3
X11Forwarding no
sudo systemctl reload sshd

Verify the restrictions are in effect without connecting:

sudo sshd -T | grep -E "passwordauthentication|permitrootlogin|permitemptypasswords|maxauthtries|x11forwarding"

Expected output:

passwordauthentication no
permitrootlogin no
permitemptypasswords no
maxauthtries 3
x11forwarding no

Note: You could also disable inbound SSH altogether with: sudo systemctl --now disable sshd

File permissions hardening:

Check if the following files are configured with the correct permissions. They should be by default:

stat -c "%a %n" /etc/shadow /etc/passwd /etc/group /root

If not, then use the following chmod commands:

chmod 700 /root
chmod 640 /etc/shadow
chmod 644 /etc/passwd /etc/group
chown root:root /etc/passwd /etc/shadow /etc/group

3. Firewall — firewalld with block Zone

Note: firewalld is installed on the Debian VM, not the Ubuntu host.

block vs drop

Zone Behavior
block Rejects unmatched incoming packets with an ICMP reject message
drop Silently drops unmatched incoming packets with no reply

Use block for a local KVM developer VM — more informative for debugging.
Use drop for internet-facing servers where you don't want to acknowledge existence.

Outbound traffic is allowed in both zones — connections initiated from within the system are unaffected.

Installation

# Remove/disable UFW first to avoid conflicts
sudo systemctl stop ufw
sudo systemctl disable ufw

# Install firewalld
sudo apt update
sudo apt install -y firewalld
sudo systemctl enable --now firewalld

# Verify
sudo firewall-cmd --state

Set block as the Default Zone

sudo firewall-cmd --set-default-zone=block

Assign Your Network Interface to the block Zone

sudo firewall-cmd --zone=block --change-interface=<your_network_interface> --permanent
sudo firewall-cmd --reload

Replace <your_network_interface> with your actual interface name (e.g. eth0, ens3, enp1s0). Find it with ip link show.

Add Optional Ports/Services

# Add SSH permanently (by port number)
sudo firewall-cmd --add-port=22/tcp --permanent

or

# Add SSH permanently (by service name)
sudo firewall-cmd --add-service=ssh --permanent
# Reload to apply
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-all

Note: You may not want to add SSH at all. As stated earlier, if no inbound SSH access is required, consider disabling it entirely with sudo systemctl --now disable sshd. If you connect exclusively via SPICE through Virtual Machine Manager and require no external access to the VM, removing SSH entirely is the right call:

sudo firewall-cmd --remove-port=22/tcp --permanent
sudo firewall-cmd --reload
sudo systemctl --now disable sshd

Note: Third-party software (remote desktop tools, VPNs, etc.) can silently add firewall rules during installation, updates, or even on service start. Always run sudo firewall-cmd --list-all after installing, updating, or removing any network-facing software to verify no unexpected ports have been opened.


4. Intrusion Prevention — fail2ban

Monitors log files and automatically bans IPs showing malicious behavior.

sudo apt install fail2ban
sudo systemctl enable fail2ban

Create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd

[sshd]
enabled = true
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
sudo systemctl restart fail2ban

5. Mandatory Access Control — AppArmor

Confines programs to a limited set of resources. Even if an attacker exploits a vulnerability, they cannot escape the profile.

AppArmor is installed and enabled by default on Debian 10 and newer. apparmor-utils is not installed by default and needs to be added separately:

sudo apt install apparmor-utils
sudo aa-enforce /etc/apparmor.d/*

# Verify status
sudo aa-status

Note: On a fresh Debian install, all profiles may already be in enforce mode. Running sudo aa-enforce /etc/apparmor.d/* is harmless in that case but may not be necessary. Verify first with sudo aa-status — if all profiles show enforce mode, no action is needed.

Warning — Docker conflict: Running sudo aa-enforce /etc/apparmor.d/* will enforce the runc AppArmor profile, which blocks Docker from loading libseccomp.so.2 and prevents containers from starting. If you are using Docker, disable the runc profile immediately after enforcing:

sudo aa-disable /etc/apparmor.d/runc
sudo systemctl restart docker

Docker manages its own container security via seccomp and per-container AppArmor profiles. The standalone runc profile is redundant and incompatible with Docker's operation.


6. Anti-Malware — ClamAV

Free, open-source, lightweight — well suited for a local development VM. Run as a scheduled scan rather than real-time to keep overhead low.

sudo apt install clamav clamav-daemon
sudo freshclam

Schedule a weekly scan via cron:

crontab -e

This opens your crontab file in your default editor. Add the following line at the bottom:

# Run ClamAV scan every Sunday at 2am
0 2 * * 0 clamscan -r /home --log=/var/log/clamav/weekly-scan.log

Save and exit. Verify the cron job was added with:

crontab -l

Note: Check /var/log/clamav/weekly-scan.log each week to review scan results and confirm the job is running.


7. Audit Logging — auditd

Provides detailed system call monitoring. Captures changes to critical system files and privilege escalation events.

sudo apt install auditd audispd-plugins
sudo systemctl enable auditd

Add rules to /etc/audit/rules.d/audit.rules:

# Monitor critical file changes
-w /etc/passwd -p wa -k passwd_changes
-w /etc/shadow -p wa -k shadow_changes
-w /etc/ssh/sshd_config -p wa -k ssh_config

# Monitor privilege escalation
-w /bin/su -p x -k privilege_escalation
-w /usr/bin/sudo -p x -k privilege_escalation

Reload auditd to apply the rules:

sudo systemctl restart auditd

8. Audit & Scoring — Lynis

Audits your entire hardening posture and produces a scored report with specific remediation items. Run periodically.

sudo apt install lynis
sudo lynis audit system

Results are automatically saved to the following files for later analysis:

File Contents
/var/log/lynis.log Full detailed log of the audit run
/var/log/lynis-report.dat Machine-readable report used for comparisons over time

9. Hide Email in Claude Code Terminal

Add to shell profile (.bashrc / .zshrc):

export CLAUDE_CODE_HIDE_ACCOUNT_INFO=1

Alternative (undocumented):

export IS_DEMO=1

10. Tool Summary

Tool Purpose
firewalld (block zone) Firewall — rejects unsolicited inbound traffic
fail2ban Brute-force IP banning
AppArmor Mandatory access control
ClamAV Anti-malware scanning
auditd System call audit logging
Lynis Hardening audit and scoring
unattended-upgrades Automatic security patches

Summary

If you've made it this far and implemented everything above, congratulations — your VM is now more secure than most production servers, and Claude Code is running in an environment that would make a penetration tester nod approvingly. Go build something great. Just remember to run sudo lynis audit system every now and then, because security is a journey, not a destination, and complacency is the most effective attack vector of all.



Appendix A: Docker Installation and Security

Docker will likely be used for sandboxed testing of agentic code, MCP servers, and API integrations. Install from Docker's official repository — not Debian's default packages, which are older and may conflict.

Step 1: Remove Conflicting Packages

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
    sudo apt-get remove $pkg
done

Step 2: Add Docker's Official Repository

# Install prerequisites
sudo apt-get update
sudo apt-get install ca-certificates curl

# Add Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Step 3: Install Docker Engine

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify the installation:

sudo docker run hello-world

Note: If this fails with a libseccomp.so.2: Permission denied or fork/exec: permission denied error, AppArmor's runc profile is blocking Docker. See the warning in Section 5 — AppArmor for the fix.

Step 4: Security Hardening

Do not add your user to the docker group. Membership in the docker group is equivalent to root access — anyone in that group can trivially escalate to root. Use sudo docker instead, or use rootless mode below.

Rootless Mode (Recommended)

Rootless mode runs the Docker daemon and all containers as a non-root user. If a container is compromised, the attacker lands as an unprivileged user on the host rather than root.

First — disable the AppArmor rootlesskit profile, which blocks rootless mode on Debian (same issue as runc):

sudo aa-disable /etc/apparmor.d/rootlesskit

Install prerequisites and set up rootless daemon (run as your user, not sudo):

sudo apt-get install -y uidmap docker-ce-rootless-extras
dockerd-rootless-setuptool.sh install

Add required environment variables to your shell profile:

echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/1002/docker.sock' >> ~/.bashrc
source ~/.bashrc

Enable Docker to start on system boot:

sudo loginctl enable-linger devop

Verify rootless mode is active:

docker info | grep -i rootless
docker run hello-world

Note: The io.max and cpuset warnings in docker info output are cosmetic — they indicate cgroup v2 I/O throttling controls are not available in the KVM guest context. This does not affect normal Docker operation.

Firewall note: Docker manipulates iptables/nftables rules directly and can bypass firewalld rules. Keep this in mind when exposing container ports — verify with:

sudo firewall-cmd --list-all

Appendix B: Running firewall-cmd Without sudo

If you are frequently adding services to the firewall allowlist over time, typing sudo for every firewall-cmd invocation can be tedious. There are two options to avoid this.

Grants passwordless firewall-cmd access for your user only:

sudo visudo -f /etc/sudoers.d/firewalld

Add this line (replace yourusername):

yourusername ALL=(ALL) NOPASSWD: /usr/bin/firewall-cmd

Option 2: PolicyKit (polkit) Rule

Grants firewall management rights via polkit — also enables GUI tools like firewall-config to work without password prompts.

Create /etc/polkit-1/rules.d/50-firewalld.rules:

polkit.addRule(function(action, subject) {
    if (action.id == "org.fedoraproject.FirewallD1" &&
        subject.isInGroup("sudo")) {
        return polkit.Result.YES;
    }
});

Security note: Either option grants your user account the ability to modify firewall rules without authentication. Anyone who gains access to your user account can alter firewall rules freely. Use with awareness of this trade-off.

👍
That's it. Hope you enjoyed it! I'm sure the document will morph over time, but this is a good starting point. until next time!
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Prowse Tech.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.