Building Kali 2026.1 Images with Packer, Proxmox and Cloud-Init

Creating automated Kali Linux templates for Proxmox should be straightforward, but in practice I spent far more time than expected getting everything working correctly together.

The combination of:

  • Packer
  • Proxmox
  • Kali Linux 2026.1
  • Debian Preseed
  • Cloud-Init
  • Proxmox Templates

required a surprising amount of trial and error before arriving at a reliable build process.

This article documents the final working configuration that successfully builds a Kali Linux 2026.1 template ready for deployment through Proxmox cloud-init.

Why Build Kali Templates with Packer?

Automating Kali image creation provides several advantages:

  • Consistent deployments
  • Repeatable infrastructure builds
  • Faster lab provisioning
  • Version-controlled templates
  • Cloud-init integration
  • Easy integration with Terraform and Ansible

Instead of manually installing Kali every time a new VM is required, Packer can automatically build a clean template that can be cloned repeatedly.

Environment

My build environment consists of:

ComponentVersion
Proxmox VE8.x
Packer1.x
Kali Linux2026.1
Cloud-InitCurrent
Debian InstallerPreseed Automated

The Packer build machine hosts a temporary HTTP server which serves the preseed and cloud-init configuration files during installation.


Directory Structure

My project structure looks similar to the following:

kali-2026.1/
├── kali-2026-1.pkr.hcl
├── credentials.pkr.hcl
├── http/
│   ├── preseed.cfg
│   ├── meta-data
│   └── user-data
└── files/
    └── 99-pve.cfg

The credentials file contains API tokens and other secrets and is intentionally excluded from source control.


Packer Configuration

The following is the final working Packer configuration used to build the Kali 2026.1 template..

# Kali 2026-1
# ---
# Packer Template to create a Kali 2026-1 on Proxmox

# Variable Definitions
variable "proxmox_api_url" {
  type = string
}

variable "proxmox_api_token_id" {
  type = string
}

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

variable "http_ip" {
  type        = string
  default     = "<your packer server ip>"
  description = "IP address to bind the HTTP server for cloud-init"
}

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

source "proxmox-iso" "kali-2026-1" {

  proxmox_url = "${var.proxmox_api_url}"
  username    = "${var.proxmox_api_token_id}"
  token       = "${var.proxmox_api_token_secret}"

  node                 = "pve-01"
  vm_id                = "8510"
  vm_name              = "kali-2026-1"
  template_description = "Kali 2026.1"

  iso_file         = "local:iso/kali-linux-2026.1-installer-amd64.iso"
  iso_storage_pool = "local"

  template_name = "packer-kali20261"

  qemu_agent = true

  scsi_controller = "virtio-scsi-pci"

  disks {
    disk_size    = "20G"
    format       = "raw"
    storage_pool = "local-lvm"
    type         = "virtio"
  }

  cores    = "4"
  cpu_type = "host"

  memory = "4096"

  network_adapters {
    model    = "virtio"
    bridge   = "vmbr0"
    firewall = "false"
  }

  cloud_init              = true
  cloud_init_storage_pool = "local-lvm"

  boot_command = [
    "<esc><wait>",
    "install preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg <wait>",
    "debian-installer=en_GB auto locale=en_GB kbd-chooser/method=gb <wait>",
    "netcfg/get_hostname=kali netcfg/get_domain=local fb=false <wait>",
    "debconf/frontend=noninteractive console-setup/ask_detect=false <wait>",
    "console-keymaps-at/keymap=gb keyboard-configuration/xkb-keymap=gb <wait>",
    "<enter><wait>"
  ]

  boot      = "c"
  boot_wait = "5s"

  http_directory    = "http"
  http_bind_address = "${var.http_ip}"

  http_port_min = 8900
  http_port_max = 8999

  ssh_username = "ansible"

  ssh_private_key_file = "/nsm/ansible/keys/juniper-hosts.key"

  ssh_timeout = "40m"
}

build {

  name    = "kali-2026-1"
  sources = ["proxmox-iso.kali-2026-1"]

  provisioner "shell" {
    inline = [
      "sudo rm /etc/ssh/ssh_host_*",
      "sudo truncate -s 0 /etc/machine-id",
      "sudo apt -y autoremove --purge",
      "sudo apt -y clean",
      "sudo apt -y autoclean",
      "sudo cloud-init clean",
      "sudo rm -f /etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg",
      "sudo rm -f /etc/netplan/00-installer-config.yaml",
      "sudo sync"
    ]
  }
}

Preseed Configuration

The installer is driven entirely through a Debian preseed file.

Placeholder: Insert preseed.cfg here

### General Config

d-i debian-installer/language string en
d-i debian-installer/country string GB
d-i debian-installer/locale string en_GB.UTF-8
d-i clock-setup/utc boolean true
d-i time/zone string Europe/London

### Keyboard Config

d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/xkb-keymap select gb

### Network Config

d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string kali-cleanroom
d-i netcfg/get_domain string local.lan
d-i netcfg/hostname string kali-cleanroom

### Root Account Setup

d-i passwd/root-login boolean true
d-i passwd/make_user boolean false
d-i passwd/root-password password RootSuperSecureTemp!
d-i passwd/root-password-again password RootSuperSecureTemp!
d-i user-setup/encrypt-home boolean false

### Ansible User Setup

d-i passwd/make-user boolean true
d-i passwd/user-fullname string Ansible user
d-i passwd/username string ansible
d-i passwd/user-password password AnsibleSuperSecureTemp!
d-i passwd/user-password-again password AnsibleSuperSecureTemp!
d-i user-setup/encrypt-home boolean false
d-i user-setup/allow-password-weak boolean false
d-i passwd/user-default-groups string audo cdrom video admin sudo

### Partitioning

d-i partman-auto/method string lvm
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm_nooverwrite boolean true

### Mirror Settings

d-i mirror/country string manual
d-i mirror/http/hostname string http.kali.org
d-i mirror/http/directory string /
d-i mirror/http/proxy string

### APT Setup

d-i apt-setup/use_mirror boolean true

### Desktop type
tasksel tasksel/first multiselect standard,core,meta-default
#tasksel tasksel/first multiselect standard,core,desktop-xfce,meta-default
d-i pkgsel/include string openssh-server build-essential sudo cloud-init 
d-i pkgsel/install-language-support boolean false
d-i pkgsel/update-policy select none
d-i pkgsel/upgrade select full-upgrade

### Bootloader

d-i grub-installer/only_debian boolean true
#d-i grub-installer/password password GrubSecret!
#d-i grub-installer/password-again password GrubSecret!
d-i grub-installer/bootdev string default


### Finishing Up

d-i preseed/late_command string \
    echo 'ansible ALL=(ALL) NOPASSWD: ALL' > /target/etc/sudoers.d/ansible ; \
    in-target chmod 440 /etc/sudoers.d/ansible ; \
    in-target apt install -y cloud-init ; \
    in-target /bin/sh -c "echo 'datasource_list: [ ConfigDrive, NoCloud ]' > /etc/cloud/cloud.cfg.d/99_datasource.cfg"; \
    in-target systemctl enable ssh.service ; \
    in-target touch /etc/cloud/cloud-init.enabled; \
    in-target systemctl daemon-reload; \
    in-target systemctl unmask cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service; \
    in-target systemctl enable cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service; \
    in-target mkdir -p /home/ansible/.ssh; \
    in-target /bin/sh -c "echo 'ssh-rsa AAAAB...' >> /home/ansible/.ssh/authorized_keys"; \
    in-target chown -R ansible:ansible /home/ansible/.ssh/; \
    in-target chmod 644 /home/ansible/.ssh/authorized_keys; \
    in-target chmod 700 /home/ansible/.ssh/; \
    in-target cloud-init clean --logs --seed; \
    in-target sh -c "truncate -s 0 /etc/machine-id";


d-i finish-install/reboot_in_progress note

A properly configured preseed file is critical because it handles:

  • Disk partitioning
  • User creation
  • Package selection
  • SSH configuration
  • Cloud-init installation
  • qemu-guest-agent installation

Cloud-Init User Data

Cloud-init is responsible for configuring cloned instances after deployment.

Placeholder: Insert user-data file here

#cloud-config
autoinstall:
  version: 1
  locale: en_GB
  keyboard:
    layout: gb
  ssh:
    install-server: true
    allow-pw: false
    disable_root: true
    ssh_quiet_keygen: true
    allow_public_ssh_keys: true
  packages:
    - qemu-guest-agent
    - sudo
  storage:
    layout:
      name: direct
    swap:
      size: 0
  user-data:
    package_upgrade: true
    timezone: Europe/London
    users:
      - name: ansible
        groups: [adm, sudo]
        lock-passwd: false
        sudo: ALL=(ALL) NOPASSWD:ALL
        shell: /bin/bash
        # passwd: your-password
        # - or -
        ssh_authorized_keys:
          - ssh-rsa AAAAB...

Cloud-Init Meta Data

Cloud-init metadata provides instance identification information.

This file is a just empty and for future use.


Proxmox Cloud-Init Configuration

Some environments also require a custom cloud-init configuration file.

Placeholder: Insert 99-pve.cfg here

datasource_list: [ConfigDrive, NoCloud]

Credentials File

The build process also relies on a separate credentials file containing:

  • Proxmox API URL
  • Token ID
  • Token Secret
  • Environment-specific variables

This file is intentionally excluded from this article and should never be committed to source control.

Example:

proxmox_api_url = "https://pve-01:8006/api2/json"  # Your Proxmox IP Address
proxmox_api_token_id = "root@pam!packer"  # API Token ID
proxmox_api_token_secret = "your packer token"

Template Cleanup for Cloud-Init

One of the most important parts of the build process happens after installation.

Before converting the VM into a template, the build removes:

rm /etc/ssh/ssh_host_*
truncate -s 0 /etc/machine-id
cloud-init clean

This ensures every cloned VM receives:

  • Unique SSH host keys
  • Unique machine ID
  • Fresh cloud-init execution

Without these cleanup steps, cloned systems can exhibit strange behaviour and duplicate identities.


Lessons Learned

The biggest challenges during this build were:

  1. Getting Kali's installer to work reliably with preseed.
  2. Ensuring cloud-init ran correctly after cloning.
  3. Removing installer-generated network configuration.
  4. Cleaning machine identity information before template conversion.
  5. Correctly serving preseed files through Packer's temporary HTTP server.
  6. Matching storage locations between ISO, VM disks and cloud-init disks.

Most online examples are based on Ubuntu, while Kali introduces several small differences that can make troubleshooting frustrating.


Final Thoughts

After a significant amount of experimentation, this build process now consistently produces a Kali Linux 2026.1 Proxmox template that works correctly with cloud-init and can be deployed repeatedly without manual intervention.

The result is a fully automated workflow where Packer creates the template, Proxmox stores it, and cloud-init customises every cloned VM during first boot.

For anyone building Kali templates in a home lab, penetration testing environment or security research platform, investing the time in a reliable Packer workflow pays dividends very quickly.


About the author

Tim Wilkes is a UK-based security architect with over 15 years of experience in electronics, Linux, and Unix systems administration. Since 2021, he's been designing secure systems for a telecom company while indulging his passions for programming, automation, and 3D printing. Tim shares his projects, tinkering adventures, and tech insights here - partly as a personal log, and partly in the hopes that others will find them useful.

Want to connect or follow along?

LinkedIn: [phpsytems]
Twitter / X: [@timmehwimmy]
Mastodon: [@timmehwimmy@infosec.exchange]


If you've found a post helpful, consider supporting the blog - it's a part-time passion that your support helps keep alive.

⚠️ Disclaimer

This post may contain affiliate links. If you choose to purchase through them, I may earn a small commission at no extra cost to you. I only recommend items and services I’ve personally read or used and found valuable.

As an Amazon Associate I earn from qualifying purchases.