As a cybersecurity enthusiast and productivity junkie, I’ve always been fascinated by the power of automation. Recently, I decided to streamline my process of setting up Kali Linux virtual machines using QEMU and Ansible. In this article, I’ll walk you through how I’ve automated the creation and configuration of Kali VMs, complete with my preferred dotfiles and tools.
The Need for Automation
If you’re like me, you probably find yourself creating new Kali VMs frequently for various projects or testing environments. The process of setting up a new VM, updating the system, installing your favorite tools, and configuring your environment can be time-consuming and repetitive. That’s where automation comes in handy. You can repurpose this setup for other projects, just change the Ansible playbook and dotfiles.
Benefits and Challenges
This automation setup offers several advantages:
- Consistent environment across all VMs
- Rapid deployment of new instances
- Version-controlled configuration (infrastructure as code)
- Easy sharing and collaboration
Some challenges I encountered:
- VirtioFS setup complexity
- Ansible role organization
- Package version management
- Dotfiles synchronization
But the benefits far outweigh the initial setup effort.
Prerequisites
Choose your preferred virtualization provider:
VirtualBox Setup
- VirtualBox
- Vagrant
- Ansible
QEMU/KVM Setup (Linux only) (My preferred setup)
- QEMU/KVM (≥ 4.2)
- Libvirt (≥ 6.2.0)
- Vagrant
- Ansible
For QEMU/KVM shared folder support:
-
Host: Linux kernel ≥ 5.4
-
Guest: Linux kernel ≥ 5.4
-
There is some additional setup required for the
vagrant-libvirt
plugin so would advise you checkout QEMU/KVM Setup Guide
Unable to resolve dependency vagrant-libvirt
error:
- +Update+ 21/02/2025
- I recently ran an update and encountered an issue where it refused to launch new VM’s. I received an error similar to the below (it was not exactly the same as I had a crash shortly after).
Error message given during initialization: Unable to resolve dependency: user requested 'vagrant-libvirt (= 0.12.2)'
after alot of searching I found this post I found following the steps resolved the issue.
- I recently ran an update and encountered an issue where it refused to launch new VM’s. I received an error similar to the below (it was not exactly the same as I had a crash shortly after).
vagrant plugin repair
vagrant plugin expunge --reinstall
vagrant plugin update
VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install vagrant-libvirt
The Automation Stack
My automation setup consists of three main components:
- Vagrant for VM provisioning
- QEMU/KVM for virtualization
- Ansible for configuration management
Let’s dive into each component.
1. Vagrant with QEMU/KVM
I chose QEMU/KVM over VirtualBox for several reasons:
- Better performance on Linux hosts
- Once you have used KVM/QEMU you will never go back to VirtualBox etc.
- Native virtualization support
- VirtioFS for efficient file sharing
The Vagrantfile handles:
- VM resource allocation (RAM, CPU)
- Network configuration
- Shared folder setup with VirtioFS
- Ansible provisioner integration
Here’s the the Vagrantfile:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "kalilinux/rolling"
config.vm.hostname = "kali-vm" # Set hostname inside VM
# Disable the default share
config.vm.synced_folder ".", "/vagrant", disabled: true
# Force libvirt provider
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'libvirt'
# QEMU/libvirt specific settings
config.vm.provider :libvirt do |libvirt|
libvirt.memory = 8192
libvirt.cpus = 4
libvirt.graphics_type = "spice"
libvirt.video_type = "qxl"
libvirt.title = "Kali Linux VM" # Set VM title in libvirt
libvirt.description = "Kali Linux VM for Security Testing" # Optional description
# Enable shared memory for VirtioFS
libvirt.memorybacking :access, :mode => "shared"
end
# Configure shared folders using VirtioFS
config.vm.synced_folder "/home/martin/.config/tmuxinator", "/home/vagrant/.config/tmuxinator",
type: "virtiofs"
config.vm.synced_folder "/home/martin/Dropbox/40-49_Career/45-KaliShared/45.02-LinuxTools", "/home/vagrant/linuxTools",
type: "virtiofs"
config.vm.synced_folder "/home/martin/Dropbox/40-49_Career/45-KaliShared/45.01-WindowsTools", "/home/vagrant/windowsTools",
type: "virtiofs"
config.vm.synced_folder "/home/martin/Dropbox/40-49_Career", "/home/vagrant/Dropbox/40-49_Career",
type: "virtiofs"
# Network settings
config.vm.network "private_network", ip: "192.168.57.11"
config.ssh.forward_x11 = true
# Provisioning configuration for Ansible
config.vm.provision "ansible" do |ansible|
ansible.playbook = "../../Ansible/configure-kali.yml"
end
end
2. Ansible Configuration
Ansible automates the entire VM setup process. The main playbook handles:
- System updates and package installation
- Development tools setup
- Security tools installation
- Terminal environment configuration (Alacritty, Tmux)
- Editor setup (Doom Emacs)
- Dotfiles deployment
Here’s the main Ansible playbook:
---
- name: Configure Kali Linux
hosts: all
become: yes # This is equivalent to using sudo
vars:
user_home: "/home/vagrant"
docker_gpg_key_url: "https://download.docker.com/linux/debian/gpg"
docker_repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable"
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Download Iosevka Nerd Font (be patient big file!)
get_url:
url: https://github.com/ryanoasis/nerd-fonts/releases/download/v3.3.0/Iosevka.zip
dest: /tmp/Iosevka.zip
mode: '0644'
- name: Install unzip if not present
apt:
name: unzip
state: present
- name: Extract Iosevka font
unarchive:
src: /tmp/Iosevka.zip
dest: "/usr/local/share/fonts/"
remote_src: yes
owner: vagrant
group: vagrant
- name: Update font cache
command: fc-cache -fv
become_user: vagrant
- name: Install base packages
apt:
name:
- eza
- alacritty
- git
- bat
- seclists
- git
- emacs
- ripgrep
- fd-find
state: present
- name: Create docker GPG directory
file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Check if Docker GPG key exists
stat:
path: /etc/apt/keyrings/docker.gpg
register: docker_gpg
- name: Add Docker GPG key
shell: |
curl -fsSL {{ docker_gpg_key_url }} | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
when: not docker_gpg.stat.exists
- name: Check if Docker repository exists
stat:
path: /etc/apt/sources.list.d/docker.list
register: docker_repo_file
- name: Add Docker repository
copy:
content: "{{ docker_repo }}"
dest: /etc/apt/sources.list.d/docker.list
when: not docker_repo_file.stat.exists
- name: Install Docker packages
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
update_cache: yes
state: present
- name: Download Starship install script
get_url:
url: https://starship.rs/install.sh
dest: /tmp/starship-install.sh
mode: '0755'
register: download_script
- name: Install Starship
shell: /tmp/starship-install.sh --yes
args:
creates: /usr/local/bin/starship
when: download_script is success
async: 180 # 5 minute timeout
poll: 5 # Check every 5 seconds
- name: Install oh-my-zsh
become_user: vagrant
shell: sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
args:
creates: "{{ user_home }}/.oh-my-zsh"
- name: Install zsh plugins
become_user: vagrant
git:
repo: "{{ item.repo }}"
dest: "{{ user_home }}/.oh-my-zsh/custom/plugins/{{ item.name }}"
loop:
- { repo: 'https://github.com/zsh-users/zsh-autosuggestions', name: 'zsh-autosuggestions' }
- { repo: 'https://github.com/zsh-users/zsh-syntax-highlighting.git', name: 'zsh-syntax-highlighting' }
- { repo: 'https://github.com/marlonrichert/zsh-autocomplete.git', name: 'zsh-autocomplete' }
- { repo: 'https://github.com/zdharma-continuum/fast-syntax-highlighting.git', name: 'fast-syntax-highlighting' }
- name: Install tmuxinator
gem:
name: tmuxinator
state: present
- name: Clone Statistically Likely Usernames
git:
repo: https://github.com/insidetrust/statistically-likely-usernames.git
dest: /usr/share/wordlists/statistically-likely-usernames
- name: Download and install Kerbrute
get_url:
url: https://github.com/ropnop/kerbrute/releases/download/v1.0.3/kerbrute_linux_amd64
dest: /usr/local/bin/kerbrute
mode: '0755'
- name: Install build dependencies for Emacs
apt:
name:
- build-essential
- libgccjit-12-dev
- libgnutls28-dev
- libjansson4
- libjansson-dev
- libncurses-dev
- libsqlite3-dev
- libgif-dev
- libjpeg-dev
- libpng-dev
- libtiff5-dev
- libxpm-dev
- libwebp-dev
- libgtk-3-dev
- texinfo
- autoconf
- automake
- pkg-config
state: present
install_recommends: no
register: pkg_result
retries: 3
delay: 5
until: pkg_result is success
- name: Install tree-sitter separately
apt:
name: libtree-sitter-dev
state: present
register: tree_sitter_result
retries: 3
delay: 5
until: tree_sitter_result is success
- name: Check if Emacs 29.4 is installed
command: emacs --version
register: emacs_version
ignore_errors: true
changed_when: false
- name: Clone Emacs repository
git:
repo: git://git.savannah.gnu.org/emacs.git
dest: /tmp/emacs
version: emacs-29.4
when: "emacs_version.rc != 0 or '29.4' not in emacs_version.stdout|default('')"
- name: Run autogen
shell: |
cd /tmp/emacs
./autogen.sh
when: "emacs_version.rc != 0 or '29.4' not in emacs_version.stdout|default('')"
- name: Configure Emacs
shell: |
cd /tmp/emacs
./configure --with-native-compilation --with-json --with-tree-sitter --with-sqlite3
args:
creates: /tmp/emacs/Makefile
when: "emacs_version.rc != 0 or '29.4' not in emacs_version.stdout|default('')"
- name: Build Emacs
shell: |
cd /tmp/emacs
make -j$(nproc)
args:
creates: /tmp/emacs/src/emacs
when: "emacs_version.rc != 0 or '29.4' not in emacs_version.stdout|default('')"
async: 1800
poll: 15
- name: Install Emacs
become: yes
shell: |
cd /tmp/emacs
make install
args:
creates: /usr/local/bin/emacs
when: "emacs_version.rc != 0 or '29.4' not in emacs_version.stdout|default('')"
async: 900
poll: 15
- name: Install other base packages
apt:
name:
- eza
- alacritty
- git
- bat
- seclists
- ripgrep
- fd-find
state: present
install_recommends: no
- name: Setup dotfiles
become_user: vagrant
block:
- name: Clone dotfiles
git:
repo: https://github.com/bloodstiller/kaliconfigs.git
dest: "{{ user_home }}/.dotfiles"
version: main # Or whatever branch you're using
update: yes # Ensures latest version is cloned
clone: yes
force: yes # This will overwrite local changes
- name: Create required directories
file:
path: "{{ item }}"
state: directory
loop:
- "{{ user_home }}/.doom.d"
- "{{ user_home }}/.config"
- name: Create symlinks
file:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
state: link
force: yes
loop:
- { src: "{{ user_home }}/.dotfiles/Zsh/.zshrc", dest: "{{ user_home }}/.zshrc" }
- { src: "{{ user_home }}/.dotfiles/Doom/config.el", dest: "{{ user_home }}/.doom.d/config.el" }
- { src: "{{ user_home }}/.dotfiles/Doom/init.el", dest: "{{ user_home }}/.doom.d/init.el" }
- { src: "{{ user_home }}/.dotfiles/Doom/packages.el", dest: "{{ user_home }}/.doom.d/packages.el" }
- { src: "{{ user_home }}/.dotfiles/Doom/README.org", dest: "{{ user_home }}/.doom.d/README.org" }
- { src: "{{ user_home }}/.dotfiles/Starship/starship.toml", dest: "{{ user_home }}/.config/starship.toml" }
- { src: "{{ user_home }}/.dotfiles/Alacritty", dest: "{{ user_home }}/.config/Alacritty" }
- { src: "{{ user_home }}/.dotfiles/Tmux/.tmux.conf", dest: "{{ user_home }}/.tmux.conf" }
- name: Install Doom Emacs
become_user: vagrant
block:
- name: Clone Doom Emacs
git:
repo: https://github.com/doomemacs/doomemacs
dest: "{{ user_home }}/.config/emacs"
depth: 1
- name: Install Doom Emacs
# This is correct and the path is correct!
shell: "{{ user_home }}/.config/emacs/bin/doom install --force"
args:
creates: "{{ user_home }}/.emacs.d/bin/doom"
async: 900
poll: 15
- name: Update Doom recipe repositories
shell: "{{ user_home }}/.config/emacs/bin/doom sync -u"
async: 900
poll: 15
- name: Enable Docker service
systemd:
name: docker
enabled: yes
state: started
- name: Add user to docker group
user:
name: vagrant
groups: docker
append: yes
- name: Clean up
apt:
autoremove: yes
clean: yes
3. Dotfiles and Tool Configuration
The automation includes configuration for:
- Alacritty terminal emulator
- Tmux with custom keybindings
- Doom Emacs with security-focused packages
- Starship cross-shell prompt
- Zsh with Oh My Zsh
Each tool has its own configuration module in the repository:
- Alacritty Configuration
- Tmux Configuration
- Doom Emacs Configuration
- Starship Configuration
- Zsh Configuration
Included Tools & Features
The automation sets up a comprehensive environment with:
Security Tools
- SecLists (lots of lists)
- Kerbrute for Kerberos authentication testing
- Statistically Likely Usernames wordlist
- Common pentesting tools (I have my folders for linux & windows tools (I will make a repo for that soon))
- Bloodhound for Active Directory analysis (dockerized)
Development Environment
- Emacs 29.4 with native compilation
- Doom Emacs configuration
- This is my main editor and I use it for everything but if you prefer another editor you can change the Ansible playbook.
- Git version control
- Docker and Docker Compose
- Build tools and development libraries
Modern Terminal Experience
- Alacritty terminal emulator
- Starship cross-shell prompt
- Tmuxinator for session management
Command Line Improvements
- Eza (modern ls replacement)
- Bat (better cat)
- Ripgrep for searching
- fd-find for file discovery
- Enhanced Zsh configuration:
- zsh-autosuggestions
- zsh-syntax-highlighting
- zsh-autocomplete
- fast-syntax-highlighting
Getting Started
To use this automation:
-
Clone the repository:
git clone https://github.com/bloodstiller/kaliconfigs.git cd kaliconfigs
-
Install prerequisites:
# For Debian/Ubuntu sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients vagrant ansible # For Arch Linux sudo pacman -S qemu-full libvirt vagrant ansible
-
Start the VM:
cd Vagrant/QEMU vagrant up
Development Workflow
For efficient development and testing:
-
Rerun only Ansible playbooks:
vagrant provision
-
Run Ansible directly against the VM:
vagrant ssh-config > vagrant-ssh-config ansible-playbook -i .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory Ansible/configure-kali.yml
-
Use snapshots for safe testing:
vagrant snapshot save baseline vagrant snapshot restore baseline
Customization Options
The automation is highly customizable:
-
VM Resources (Vagrantfile):
config.vm.provider :libvirt do |libvirt| libvirt.memory = 8192 libvirt.cpus = 4 end
-
Package Selection (Ansible):
- name: Install base packages apt: name: - eza - alacritty - git # Add your packages here
-
Shared Folders:
config.vm.synced_folder "/path/to/host", "/path/in/guest", type: "virtiofs"
Documentation & Resources
For detailed setup and configuration instructions, refer to:
- Main Project Documentation - Overview and quick start guide
- Ansible Configuration Guide - Detailed playbook customization
- QEMU/KVM Setup Guide - QEMU-specific installation and configuration
- Alacritty Configuration - Terminal emulator setup
- Doom Emacs Configuration - Editor configuration
- Tmux Configuration - Terminal multiplexer setup
- Starship Configuration - Shell prompt customization
- Zsh Configuration - Shell configuration and plugins
Conclusion
Automating Kali VM setup with QEMU and Ansible has significantly improved my workflow. The combination of Vagrant for VM management, QEMU for virtualization, and Ansible for configuration provides a powerful and flexible automation stack.
The complete code is available in my kaliconfigs repository . Feel free to fork it and adapt it to your needs.
Happy hacking!