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 withsudo apt update && sudo apt upgradeqemu-kvm— QEMU hypervisor with KVM accelerationqemu-system— full system emulation supportqemu-utils— QEMU disk image utilities (qemu-img, etc.)libvirt-daemon-system— libvirt daemon and default configurationlibvirt-clients— virsh and other libvirt client toolslibguestfs-tools— tools for manipulating VM disk imagesbridge-utils— network bridge managementvirtinst— virt-install command for creating VMsvirt-manager— graphical VM management interface
- Restrict access to
libvirt— only your user should be in thelibvirtgroup- Check this with the
getent group libvirtcommand.
- Check this with the
- 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
sscommands return nothing andlibvirtd.confshowslisten_tcp = 0or the listen lines are commented out, you are not exposed.
- Check if libvirtd is listening on any TCP port:
- AppArmor is already active on Ubuntu and has a KVM/QEMU profile by default — verify with
sudo aa-status. In the output, look forlibvirtd,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.1by 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-allafter 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 withsudo aa-status— if all profiles show enforce mode, no action is needed.
Warning — Docker conflict: Running
sudo aa-enforce /etc/apparmor.d/*will enforce theruncAppArmor profile, which blocks Docker from loadinglibseccomp.so.2and 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 dockerDocker manages its own container security via seccomp and per-container AppArmor profiles. The standalone
runcprofile 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.logeach 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 deniedorfork/exec: permission deniederror, AppArmor'sruncprofile 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.maxandcpusetwarnings indocker infooutput 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.
Option 1: Targeted sudoers Rule (Recommended)
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.