Having spend way too much time perfecting my personal development setup, I've reached a point, where it annoys me to not be able to use the muscle memory, keyboard shortcuts and tools I have gotten used to. Like probably most Linux users I started out with a ready-made Linux distribution, I think it was Debian in my case. Over the years I have tried countless distributions, window managers, editors, themes, shells and tools. Some of them stuck with me, others proved not to be what I was looking for.


This "natural selection" has stabilized in some instances, but I still try to optimize workflows where I can and look into new stuff on a regular basis. While I would like to think differently, critics might say I could have spent all the time I tried to fine-tune vim plugins to save a few keystrokes on writing actual code and would have been more productive. This might be true, but let's face it: I had and still have a lot of fun on the way, have learned about things I would never have discovered.

This endless rabbit hole even led me to build my own keyboard and program it with a modified firmware along with learning the colemak keyboard layout, just to name a few.

The plan

While Kali Linux is a great tool for security research and pentesting, I have trying to avoid using it for the reasons above. It's great, but it seemed easier in most cases to just install an AUR package on arch Linux, than to have to work in an uncustomized VM.

This approach is starting to reach its limits. My OS is starting to get full of one-use tools I installed for a particular use-case and forgot to get rid of afterwards. Also, quite a lot of them need root privileges to work properly and I don't feel like running everything on my main installation.


My dotfiles have been managed with Ansible for some time now. In conjunction with Vagrant this seemed like a good solution to create Kali VMs with one command, use them and dispose them to create them fresh if something goes sideways.

I adapted them and created a Vagrantfile with an Ansible playbook that allows me to generate and start a fresh Kali VM with one command, including all my personal customizations. And here is how I did it.


Ansible: Wikipedia says: Ansible is an open-source software provisioning, configuration management, and application-deployment tool. I will be using it to automate all the setup steps I would normally have to do manually on a fresh install to get it in the state I want it.

Vagrant: Wikipedia (again): Vagrant is an open-source software product for building and maintaining portable virtual software development environments.

The setup will be based around the i3 tiling window manager and my dotfiles.


Kali has announced officially maintained Images on Vagrant Cloud.

The so called Vagrantfile defines a VM based on some Image. If you wanted to just use Kali in its default configuration, you could just start the Vagrant file provided and be done.

To get it, just create an empty directory and initialize it with vagrant init

vagrant init kalilinux/rolling

This will create a Vagrantfile that can be used to spawn new VMs with ease. Vagrant itself doesn't do the virtualization, it only manages it. While multiple virtualization backends are supported, I opted for the default: VirtualBox.

There are a few basic settings I changed to add a shared folder to the VM, set the amount of RAM it can use and enable the GUI.

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder.

  config.vm.synced_folder "~/CTF", "/vagrant_data"

  # VirtualBox settings
  config.vm.provider "virtualbox" do |vb|
    # Display the VirtualBox GUI when booting the machine
    vb.gui = true

    # Customize the amount of memory on the VM:
    vb.memory = "4096"

Later on, I encountered an error that occurs from time to time and prevents Ansible from finding the playbook we will add. There is an issue on their GitHub page about it and I'm still not sure if it's a proper bug. The solution was to add this line just before the final end in the Vagrantfile.

# Workaround to make Ansible find the playbook
vagrant_synced_folder_default_type = ""

Now comes the most important customization of the file, adding Ansible provisioning. The Vagrantfile is a plain ruby file, but even without knowing the language it should be mostly self-explanatory.

We use the :ansible_local build-in provisioning. This allows for Ansible to be executed inside the VM. That way, you don't have to even have Ansible installed on your system. The ansible.install = true takes care of magically installing it when needed in the VM.

config.vm.provision :ansible_local do |ansible|
  ansible.playbook = "playbook.yml"
  ansible.verbose = true
  ansible.install = true
  ansible.limit = "all"
  ansible.galaxy_role_file = "requirements.yml"
  ansible.galaxy_roles_path = "roles"
  ansible.become = true

If you have worked with Ansible before, you will know what the playbook.yml and requirements.yml files are. Here we just instruct Ansible to look for them in the current directory. Internally Vagrant creates a shared folder and copies those files to the VM so Ansible can run on it.

And that's it for the Vagrantfile. We now only have to create our Ansible project to set everything up. One note about Ansible's inventory: Vagrant will generate that for us dynamically. The ansible.limit = "all" directive takes care of everything else.


Let's start with what I already had. To manage my dotfiles on my main OS I have created already roles that take care of setting up different applications. This can be added to the roles list in the playbook and will be executed. If you want to look into what each of them does, look at my dotfiles repository.

  - ansible-xresources
  - ansible-i3
  - ansible-vim
  - ansible-xfce4-terminal
  - ansible-tmux
  - ansible-zsh
  - ansible-rofi

These roles only set up the configuration files for my personal user and do not actually install the tools. I kept them that way to make them compatible with different distributions and being able to deploy them on systems where I don't have root privileges to install stuff. For this reason, we need to install the packages that are not in Kali per default.

I added a task with a list of packages to be installed via apt. It's easy to add or remove entries here. The list is not that long so putting them in a separate file didn't make sense at this point.

- name: Install missing packages
      - i3-wm
      - i3lock
      - i3status
      - zsh
      - neovim
      - apt-file
      - htop
      - rofi
      - nodejs
      - nitrogen
    state: latest
    update_cache: yes

Since my font of choice, Roboto Mono is not available in the repositories I also added a straight forward task to pull it from google and place it in the correct location. It includes a creates directive that checks if the fonts are already present and skips downloading if true.

- name: Install fonts and update font cache
  script: install_fonts.sh
    creates: /usr/share/fonts/truetype/robotomono

The script just downloads the font files to the correct folder and updates the font cache. The --content-disposition enables the use of "Content-Disposition" headers to describe what the name of a downloaded file should be.

#!/usr/bin/env bash

# Download the files
wget --content-disposition -P \
	/usr/share/fonts/truetype/robotomono \

# Update font cache
fc-cache -f -v

I also set the shell of root to zsh in a separate task.

- name: Set shell of user root to zsh
    name: root
    shell: /bin/zsh

The last two tasks are not absolutely necessary but make the experience a bit more enjoyable. I guess you can guess what they do.

- name: Copy wallpaper
    src: wallpaper.png
    dest: /usr/share/backgrounds/kali/kali-i3.png
- name: Set wallpaper
    src: nitrogen.cfg
    dest: "{{ansible_env.HOME}}/.config/nitrogen/bg-saved.cfg"

While talking about eye-candy I should add, that all my dotfiles are based on the great base16 color schemes by chriskemptson. You can find all the supported applications in this list. Each color scheme is defined is made up of 16 colors and has it's own repository. More specifically, the color schemes definitions are defined in yaml syntax, which is great because Ansible can use it in its templates directly.

To use a different color scheme just pick one that you like and copy it to the group_vars/all file in the Ansible project. I chose onedark, you can see the result in the screenshot above.

# group_vars/all
scheme: "OneDark"
scheme: "base16-onedark"
author: "Lalit Magant (http://github.com/tilal6991)"

base00: "282c34"
base01: "353b45"
base0F: "be5046"

Finally there are a few differences between the configuration files I use on my main installation and the ones inside the VM. For this purpose I added a few control variables to the vars: section of the playbook. Setting these values will change the outcome of the rendered templates.

As an example, I use the i3-gaps fork of i3 on my system, which has some patches applied to it. While there is a Linux Arch package for it, there is none in the repositories of Kali. The configuration is mostly compatible, except for the gaps directives, which are provided by one of the patches.

In this case the i3_use_gaps variable is set to false in the playbook (default value being true) explicitly like this:

- i3_use_gaps: false
- i3_terminal: xfce4-terminal
- i3_use_polybar: false
- i3_optional: false

The relevant parts of my i3 configuration template are fenced with special jinja2 directives, which evaluate those and only include the output between them if the value is true.

{% if i3_use_gaps %}
gaps inner 6
gaps outer 0
smart_gaps on
smart_borders no_gaps
{% endif %}

A note about Ansible Galaxy

Ansible Galaxy provides prepackaged units of work known to Ansible as roles. Think of it like a sharing platform for Ansible roles, directly integrated with GitHub. This allows us to specify a requirements.yml file with all the roles needed for the playbook. Vagrant also passes this file to Ansible which in turn automatically downloads them, so they can be used.

The syntax is pretty straightforward and just defines where to get them from, the name and the protocol to use. Here is an example for the i3 role:

- src: https://github.com/pinpox/ansible-i3.git
  name: ansible-i3
  scm: git

Give me the VM already!

And that's it. If all that sounded complicated to just create a virtual machine, be relived to know that you now just need one single command to generate the VM:

vagrant up

This command will automatically download the Kali Image, create a VirtualBox machine, download the roles, copy them over, start the machine, execute the Ansible project and greet you with your finished box to log in.

In case you make changes to the playbook after the VM has already been created, you can run the modified playbook again without the need to create a new machine with vagrant provision

... Profit?

You can find the final result here with a bit of additional documentation. Be warned that this was created for personal purposes and you might have to adapt it. If you find any bugs or want something else added be free to drop me an issue or send me a pull request.