Skip to content

Stop Re-inventing the Wheel

Background

I am tired of re-establishing a fresh version of Kali each time I want to just start fresh. As a student, I'm constantly trying out different tools and futzing around with file structure on this particular box as a work through HTB or other CTF challenges. While I am more intentional on other boxes, I want my Kali box to be easily refreshed with the latest version of Kali and re-established with all of my preferred tools/repos, configurations, etc. and also be established on my hypervisor with the same network configurations. This seemed like a good opportunity to practice some IaC tools previoulsy mentioned in courses like SANS SEC488, SEC530, etc.

If you don't care about the guide/walkthrough, you can just download the repo here

Overview

This guide shows you how to build a fully automated, repeatable Kali VM pipeline on Proxmox using:

  1. Packer to build a golden template
  2. Terraform to clone that template and trigger Ansible
  3. Ansible to finalize configuration (tools, dotfiles, Tailscale)

All commands run from a control host (your laptop or CI), not on the Proxmox hypervisor.


Project Layout

project-root/
├── packer/
│   ├── kali-proxmox-template.pkr.hcl
│   └── files/                  ← static scripts or assets
├── terraform/
│   ├── variables.tf
│   ├── main.tf
│   └── backend.tf              ← (optional Terraform Cloud / remote backend)
└── ansible/
    └── provision.yml
To create this file structure from your pwd:
mkdir .fresh_kali && mkdir .fresh_kali/packer .fresh_kali/terraform .fresh_kali/packer/files .fresh_kali/ansible && cd .fresh_kali && touch packer/kali-proxmox-template.pkr.hcl terraform/variables.tf terraform/main.tf terraform/backend.tf ansible/provision.yml


1. Packer: Golden Template

Packer is a tool for automating the creation of machine images; in this walkthrough we’re using it to build a golden Kali Linux VM template on Proxmox, complete with SSH keys and initial user setup.

packer/kali-proxmox-template.pkr.hcl

Below is your Packer template, with a helper script (packer/files/get_latest_kali_iso.sh) that fetches the most recent live ISO URL automatically at build time.

packer {
  required_plugins {
    proxmox = {
      source  = "github.com/hashicorp/proxmox"
      version = "~> 1.2"
    }
  }
}

variable "proxmox_password" {
  type      = string
  sensitive = true
}

variable "ssh_private_key" {
  type        = string
  description = "Path to private SSH key for root or deploy user"
  default     = "~/.ssh/id_rsa"
}

variable "kali_iso_url" {
  type        = string
  description = "Kali ISO URL; override manually if needed"
  default     = ""
}

source "proxmox-iso" "kali" {
  # API connection
  proxmox_url              = "https://proxmox.local:8006/api2/json" # you should put in whatever your proxmox url is - so if you are just using your IP, you'd use that.
  username                 = "root@pam"
  password                 = var.proxmox_password
  insecure_skip_tls_verify = true

  # Which node to build on
  node                     = "pve"  # you may have renamed this to something else so ensure it is the name of your node

  # VM identity
  vm_id                    = 9000
  vm_name                  = "packer-kali-{{timestamp}}"

  # Hardware
  cores                    = 4
  memory                   = 8192

# Boot ISO
boot_iso {
  type     = "scsi"
  iso_file = var.kali_iso_url != "" ? var.kali_iso_url : "local:iso/kali-default.iso"
  unmount  = true
}

  # Disk
  disks {
    type         = "scsi"
    disk_size    = "64G"
    storage_pool = "local-lvm"
  }

  # Network
  network_adapters {
    model  = "virtio"
    bridge = "vmbr0"
  }

  # SSH
  ssh_username         = "root"
  ssh_private_key_file = var.ssh_private_key
  ssh_timeout          = "20m"
}

build {
  name    = "kali-proxmox-template"
  sources = ["source.proxmox-iso.kali"]

  provisioner "shell" {
    inline = [
      "apt update && apt install -y sudo zsh git curl",
      "useradd -m -s /usr/bin/zsh deploy",
      "echo 'deploy ALL=(ALL) NOPASSWD: /usr/bin/apt-get,/usr/bin/git,/usr/bin/systemctl' > /etc/sudoers.d/deploy",
      "chmod 440 /etc/sudoers.d/deploy",
      "runuser -l deploy -c 'git clone https://github.com/youruser/dotfiles.git ~/dotfiles'",
      "runuser -l deploy -c 'ln -sf ~/dotfiles/.zshrc ~/.zshrc'",
      "curl -fsSL https://tailscale.com/install.sh | sh",
      "systemctl enable --now tailscaled"
    ]
  }

  provisioner "shell" {
    inline = [
      "sed -ri 's/^#?PermitRootLogin\\s+.*/PermitRootLogin no/' /etc/ssh/sshd_config",
      "systemctl reload sshd"
    ]
  }

  # No post-processor needed; proxmox-iso leaves you with a template
}

packer/files/get_latest_kali_iso.sh

The goal of this script is to generate the URL of the latest Kali Linux ISO file for the live-amd64 version. If your goal is to use a different version, you would want to modify this script accordingly. This script is run in order to generate the iso_file variable used in the file packer/kali-proxmox-template.pkr.hcl.

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="https://cdimage.kali.org/current/"
SHAFILE_URL="${BASE_URL}SHA256SUMS"

# 1) Find the ISO filename
ISO_FILENAME=$(curl -fsSL "${BASE_URL}" \
  | grep -oE 'href="kali-linux-[0-9]+\.[0-9]+(\.[0-9]+)?-live-amd64\.iso"' \
  | sed -E 's/href="([^"]+)"/\1/' \
  | head -n1)

# 2) Build the full URL
ISO_URL="${BASE_URL}${ISO_FILENAME}"

# 3) Extract the matching checksum
ISO_CHECKSUM=$(curl -fsSL "${SHAFILE_URL}" \
  | grep " ${ISO_FILENAME}\$" \
  | awk '{print $1}')

# 4) Export them for the caller
export ISO_URL
export ISO_CHECKSUM

Once both files are populated, you would run:

./files/get_latest_kali_iso.sh
packer init . # this command only needs to be run the first time
packer build \
  -var="proxmox_password=$PM_PASS" \
  -var="kali_iso_url=$ISO" \
  kali-proxmox-template.pkr.hcl

# This will set ISO_URL and ISO_CHECKSUM in your shell environment
source files/get_latest_kali_iso.sh

# Debug — ensure they’re set
echo "ISO_URL     = <${ISO_URL}>"
echo "ISO_CHECKSUM = <${ISO_CHECKSUM}>"

# Now run Packer
packer init . # this command will only need to be run once. 
packer build \
  -var="proxmox_password=${PM_PASS}" \
  -var="kali_iso_url=${ISO_URL}" \
  -var="kali_iso_checksum=${ISO_CHECKSUM}" \
  kali-proxmox-template.pkr.hcl
This will download the iso onto your control host and then apply the build instructions to create the template. Next step is taking that template and using it to create the actual VM with the git repo tools we want on it.

Note: If you are not caring to update this file later, you can just rebuild the VM from the existing template after you've created it.

Control Host has ISO file, but file did not upload to proxmox

scp packer/downloaded_iso_path/*.iso root@proxmox.local:/var/lib/vz/template/iso/
You can manually move the file over, but then you'll need to just run the build again with modification: This new hcl file just removes the variables related to the url, references the local file instead, and adjusts the Boot section to reflect the changes.

packer init .
packer build \
  -var="proxmox_password=$PM_PASS" \
  kali-proxmox-template-on-proxmox-already.pkr.hcl

2. Terraform: Clone & Trigger Ansible

Terraform is a declarative “infrastructure as code” engine; following the image build you’d use it to define and spin up Proxmox VMs (and any networking or storage) based on that template in a repeatable, version‑controlled way.

terraform/backend.tf (optional)

Use a remote backend for state & secrets:

terraform {
  backend "remote" {
    hostname     = "app.terraform.io"
    organization = "your-org"
    workspaces {
      name = "kali-pipeline"
    }
  }
}

terraform/variables.tf

variable "proxmox_password" { type = string }

Variable is pulled from TF_VAR_proxmox_password env var or your backend.

Dynamic Template Lookup with External Data

data "external" "latest_template" {
  program = ["bash", "-c", <<-EOT
    pvesh get /nodes/pve-node1/qemu --output-format=json | \
      jq -r '.[] | select(.template==1 and .name|startswith("kali-template-")) | .name' | \
      sort | tail -n1 | jq -R '{name: .}'
  EOT]
}
    pvesh get /nodes/pve-node1/qemu --output-format=json | \
      jq -r '.[] | select(.template==1 and .name|startswith("kali-template-")) | .name' | \
      sort | tail -n1 | jq -R '{name: .}'
  EOT]
}

This requires the Proxmox CLI (pvesh) and jq on your control host.

terraform/main.tf

provider "proxmox" {
  pm_api_url = "https://proxmox.local:8006/api2/json"
  pm_user    = "root@pam"
  pm_password= var.proxmox_password
}

data "external" "latest_template" {}

resource "proxmox_vm_qemu" "kali_ephemeral" {
  name        = "kali-${timestamp()}"
  target_node = "pve-node1"
  clone       = data.external.latest_template.result.name

  cores  = 4
  memory = 8192

  disk {
    size    = "64G"
    type    = "scsi"
    storage = "local-lvm"
  }

  network {
    model  = "virtio"
    bridge = "vmbr0"
    tag    = 42
  }

  # SSH key: copies your public key (~/.ssh/id_rsa.pub) into the VM's authorized_keys for the deploy user
  sshkeys = file("~/.ssh/id_rsa.pub")
}

resource "null_resource" "ansible_provision" {
  depends_on = [proxmox_vm_qemu.kali_ephemeral]

  provisioner "local-exec" {
    command = <<-EOT
      ansible-playbook \
        -i '${proxmox_vm_qemu.kali_ephemeral.ipv4_address},' \
        --user=deploy \
        --private-key=~/.ssh/id_rsa \
        ../ansible/provision.yml
    EOT
  }
}

Run (from project-root/terraform):

terraform init    # once or when providers change
terraform apply -var="proxmox_password=$PM_PASS" -auto-approve

3. Ansible: Final Configuration

Ansible is an agentless configuration‑management system; after Terraform provisions the VM, Ansible applies playbooks to configure system settings, install packages like Tailscale, lock down SSH, and enforce your desired state.

ansible/provision.yml

- name: Finalize Kali ephemeral VM
  hosts: all
  become: true

  tasks:
    - name: Update APT cache
      apt:
        update_cache: yes

   - name: Install extra tools
      apt:
        name:
          - htop
          - nmap
          - python3-pip
          - neo4j
        state: latest

    - name: Clone BloodHound
      git:
        repo: https://github.com/BloodHoundAD/BloodHound.git
        dest: /opt/BloodHound
        version: master
        force: yes

    - name: Clone BloodyAD
      git:
        repo: https://github.com/0xmolox/BloodyAD.git
        dest: /opt/BloodyAD
        version: master
        force: yes

    - name: Clone CrackMapExec (CME)
      git:
        repo: https://github.com/byt3bl33d3r/CrackMapExec.git
        dest: /opt/CrackMapExec
        version: master
        force: yes

    - name: Install CrackMapExec requirements
      pip:
        requirements: /opt/CrackMapExec/requirements.txt
        executable: pip3

    - name: Symlink .zshrc
      file:
        src: /home/deploy/dotfiles/.zshrc
        dest: /home/deploy/.zshrc
        state: link

    - name: Install Python pip packages
      pip:
        name:
          - pwntools
          - yara
        executable: pip3

    - name: Ensure Tailscale is running
      service:
        name: tailscaled
        state: started
        enabled: true

    - name: Authenticate Tailscale (if provided)
      shell: |
        tailscale up --authkey "${TS_AUTHKEY}" --hostname "${inventory_hostname}"
      when: "${TS_AUTHKEY}" != ""

Secrets: Export

export TF_VAR_proxmox_password="<your-secret>"
export SSH_PASSWORD="<your-ssh-password>"
export TS_AUTHKEY="<tailscale-auth-key>"
  • TF_VAR_proxmox_password from your Proxmox API token or root password
  • SSH_PASSWORD only if you’re using password auth (otherwise omit)
  • TS_AUTHKEY from the Tailscale admin console under Auth Keys

Day‑to‑Day Usage

Refresh Template (Optional)

cd packer
env PM_PASS=$PM_PASS SSH_PASS=$PM_PASS
./files/get_latest_kali_iso.sh
packer build \
  -var="proxmox_password=$PM_PASS" \
  -var="kali_iso_url=$ISO" \
  kali-proxmox-template.pkr.hcl
  ```

### Spin Up & Provision
```bash
cd ../terraform
env TF_VAR_proxmox_password=$PM_PASS terraform apply -auto-approve

Teardown

When done:

terraform destroy -auto-approve

All steps are run on your control host. No need to log into Proxmox shell or operate directly on the hypervisor. Environment variables are limited to the terminal's session. They will disappear when the terminal is closed - which is what we want given they include credentials.