diff --git a/README.md b/README.md
index 6c0e9d53..f282b89d 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@ Read more about how [custom images](https://maas.io/docs/how-to-customise-images
| CentOS 7 | Stable | >= 2.3 |
| CentOS 8 | EOL | >= 2.7 |
| CentOS 8 Stream | Beta | >= 3.2 |
+| Debian 10 | Beta | >= 3.3 |
+| Debian 11 | Beta | >= 3.3 |
+| Debian 12 | Beta | >= 3.3 |
| OL8 | Beta | >= 3.5 |
| OL9 | Beta | >= 3.5 |
| RHEL 7 | EOL | >= 2.3 |
diff --git a/debian/Makefile b/debian/Makefile
new file mode 100644
index 00000000..f0c378dd
--- /dev/null
+++ b/debian/Makefile
@@ -0,0 +1,67 @@
+#!/usr/bin/make -f
+
+include ../scripts/check.mk
+
+PACKER ?= packer
+PACKER_LOG ?= 0
+export PACKER_LOG
+
+SERIES ?= bullseye
+BOOT ?= uefi
+ARCH ?= amd64
+
+ifeq ($(strip $(SERIES)),buster)
+ VERSION = 10
+else ifeq ($(strip $(SERIES)),bullseye)
+ VERSION = 11
+else ifeq ($(strip $(SERIES)),bookworm)
+ VERSION = 12
+else
+ VERSION = 11
+endif
+
+# Safeguard
+ifeq ($(strip $(ARCH)),arm64)
+ boot = uefi
+endif
+
+.PHONY: all clean
+
+all: debian
+
+$(eval $(call check_packages_deps,cloud-image-utils ovmf,cloud-image-utils ovmf))
+
+lint:
+ packer validate .
+ packer fmt -check -diff .
+
+format:
+ packer fmt .
+
+OVMF_VARS.fd: /usr/share/OVMF/OVMF_VARS.fd
+ cp -v $< $@
+
+debian: check-deps clean
+ ${PACKER} init . && ${PACKER} build \
+ -var debian_series=${SERIES} \
+ -var debian_version=${VERSION} \
+ -var architecture=${ARCH} \
+ -var boot_mode=${BOOT} .
+
+clean:
+ ${RM} -rf output-* debian-custom-*.gz \
+ seeds-cloudimg.iso \
+ OVMF_VARS.fd \
+ AAVMF_VARS.fd
+
+CUSTOM_PKGS:=${wildcard packages/*.deb}
+
+packages/custom-packages.tar.gz: ${CUSTOM_PKGS}
+ifeq ($(strip $(CUSTOM_PKGS)),)
+ tar czf $@ -C packages -T /dev/null
+else
+ tar czf $@ -C packages ${notdir $^}
+endif
+
+.INTERMEDIATE: OVMF_VARS.fd packages/custom-packages.tar.gz \
+ seeds-cloudimg.iso
diff --git a/debian/README.md b/debian/README.md
new file mode 100644
index 00000000..b28569ee
--- /dev/null
+++ b/debian/README.md
@@ -0,0 +1,147 @@
+# Debian Packer Templates for MAAS
+
+## Introduction
+
+The Packer templates in this directory creates Debian images for use with MAAS.
+
+## Prerequisites (to create the image)
+
+* A machine running Ubuntu 18.04+ with the ability to run KVM virtual machines.
+* qemu-utils, libnbd-bin, nbdkit and fuse2fs
+* qemu-system
+* ovmf
+* cloud-image-utils
+* [Packer](https://www.packer.io/intro/getting-started/install.html), v1.7.0 or newer
+
+## Requirements (to deploy the image)
+
+* [MAAS](https://maas.io) 3.2+
+* [Curtin](https://launchpad.net/curtin) 21.0+
+* [A Custom Preseed for Debian (Important - See below)]
+
+## Supported Debian Versions
+
+The builds and deployment has been tested on MAAS 3.3.5 with Jammy ephemeral images,
+in BIOS and UEFI modes. The process currently works with the following Debian series:
+
+* Debian 10 (Buster)
+* Debian 11 (Bullseye)
+* Debian 12 (Bookworm)
+
+## Supported Architectures
+
+Currently amd64 (x86_64) and arm64 (aarch64) architectures are supported with aemd64
+being the default.
+
+## Known Issues
+
+* UEFI images fro Debian 10 (Buster) and 11 (Bullseye) are usable on both BIOS and
+UEFI systems. However for Debian 12 (Bookworm) explicit images are required to
+support BIOS and UEFI modes. See BOOT make parameter for more details.
+
+
+## debian-cloudimg.pkr.hcl
+
+This template builds a tgz image from the official Debian cloud images. This
+results in an image that is very close to the ones that are on
+.
+
+### Building the image
+
+The build the image you give the template a script which has all the
+customizations:
+
+```shell
+packer init .
+packer build -var customize_script=my-changes.sh -var debian_series=bullseye \
+ -var debian_version=11 .
+```
+
+`my-changes.sh` is a script you write which customizes the image from within
+the VM. For example, you can install packages using `apt-get`, call out to
+ansible, or whatever you want.
+
+Using make:
+
+```shell
+make debian SERIES=bullseye
+```
+
+#### Accessing external files from you script
+
+If you want to put or use some files in the image, you can put those in the `http` directory.
+
+Whatever file you put there, you can access from within your script like this:
+
+```shell
+wget http://${PACKER_HTTP_IP}:${PACKER_HTTP_PORT}:/my-file
+```
+
+### Installing a kernel
+
+If you do want to force an image to always use a specific kernel, you can
+include it in the image.
+
+The easiest way of doing this is to use the `kernel` parameter:
+
+```shell
+packer init .
+packer build -var kernel=linux-image-amd64 -var customize_script=my-changes.sh .
+```
+
+You can also install the kernel manually in your `my-changes.sh` script.
+
+### Custom Preseed for Debian
+
+As mentioned above, Debian images require a custom preseed file to be present in the
+preseeds directory of MAAS region controllers.
+
+When used snaps, the path is /var/snap/maas/current/preseeds/curtin_userdata_custom
+
+Example ready to use preesed files has been included with this repository. Please
+see curtin_userdata_custom_amd64 and curtin_userdata_custom_arm64.
+
+Please be aware that this could potentially create a conflict with the rest of custom
+images present in your setup, hence a through investigation and testing might be
+required prior to deployment.
+
+To work around a conflict, it is possible to name the preseed file something similar to
+curtin_userdata_custom_amd64_generic_debian-10 assuming the architecture was set to
+amd64/generic and the uploaded name was set to custom/debian-10.
+
+### Makefile Parameters
+
+#### PACKER_LOG
+
+Enable (1) or Disable (0) verbose packer logs. The default value is set to 0.
+
+#### SERIES
+
+Specify the Debian Series to build. The default value is set to bullseye.
+
+#### BOOT
+
+Supported boot mode baked into the image. The default is set to uefi. Please
+see the Known Issues section for more details. This parameter is only valid
+for amd64 architecture.
+
+#### ARCH
+
+Target image architecture. Supported values are amd64 (default) and arm64.
+
+### Default Username
+
+The default username is ```debian```
+
+## Uploading images to MAAS
+
+TGZ image
+
+```shell
+maas admin boot-resources create \
+ name='custom/debian-12' \
+ title='Debian 12 Custom' \
+ architecture='amd64/generic' \
+ filetype='tgz' \
+ content@=debian-custom-cloudimg.tar.gz
+```
diff --git a/debian/curtin_userdata_custom_amd64 b/debian/curtin_userdata_custom_amd64
new file mode 100644
index 00000000..bc605986
--- /dev/null
+++ b/debian/curtin_userdata_custom_amd64
@@ -0,0 +1,24 @@
+#cloud-config
+kernel:
+ fallback-package: linux-image-amd64
+ package: linux-image-amd64
+
+apt:
+ preserve_sources_list: true
+
+debconf_selections:
+ maas: |
+ {{for line in str(curtin_preseed).splitlines()}}
+ {{line}}
+ {{endfor}}
+
+late_commands:
+ maas: [wget, '--no-proxy', '{{node_disable_pxe_url}}', '--post-data', '{{node_disable_pxe_data}}', '-O', '/dev/null']
+ late_1: mount --bind $TARGET_MOUNT_POINT /mnt
+ late_2: grep -A2 datasource /etc/cloud/cloud.cfg.d/91_kernel_cmdline_url.cfg | sed 's/curtin//' | tee /mnt/etc/cloud/cloud.cfg.d/debian.cfg
+ late_3: sed -i 's@ubuntu.com/ubuntu@debian.org/debian@g;s@archive@deb@g;s@ubuntu@debian@g;s@Ubuntu@Debian@g;' /mnt/etc/cloud/cloud.cfg
+ late_4: debver=$(cat /mnt/etc/debian_version | awk -F. '{print $1}'); if [ ${debver} -eq 10 ]; then rel="buster"; elif [ ${debver} -eq 11 ]; then rel="bullseye"; elif [ ${debver} -eq 12 ]; then rel="bookworm"; fi; sed -i s/stable/${rel}/g /mnt/etc/apt/sources.list;
+ late_5: sed -i '/^set -e/{n;N;d}' /mnt/etc/kernel/postinst.d/zz-update-grub
+ late_6: rm -f /usr/local/bin/dpkg-query
+ late_7: rm -f /usr/local/bin/netplan
+
diff --git a/debian/curtin_userdata_custom_arm64 b/debian/curtin_userdata_custom_arm64
new file mode 100644
index 00000000..3a475ec6
--- /dev/null
+++ b/debian/curtin_userdata_custom_arm64
@@ -0,0 +1,24 @@
+#cloud-config
+kernel:
+ fallback-package: linux-image-arm64
+ package: linux-image-arm64
+
+apt:
+ preserve_sources_list: true
+
+debconf_selections:
+ maas: |
+ {{for line in str(curtin_preseed).splitlines()}}
+ {{line}}
+ {{endfor}}
+
+late_commands:
+ maas: [wget, '--no-proxy', '{{node_disable_pxe_url}}', '--post-data', '{{node_disable_pxe_data}}', '-O', '/dev/null']
+ late_1: mount --bind $TARGET_MOUNT_POINT /mnt
+ late_2: grep -A2 datasource /etc/cloud/cloud.cfg.d/91_kernel_cmdline_url.cfg | sed 's/curtin//' | tee /mnt/etc/cloud/cloud.cfg.d/debian.cfg
+ late_3: sed -i 's@ubuntu.com/ubuntu@debian.org/debian@g;s@archive@deb@g;s@ubuntu@debian@g;s@Ubuntu@Debian@g;' /mnt/etc/cloud/cloud.cfg
+ late_4: debver=$(cat /mnt/etc/debian_version | awk -F. '{print $1}'); if [ ${debver} -eq 10 ]; then rel="buster"; elif [ ${debver} -eq 11 ]; then rel="bullseye"; elif [ ${debver} -eq 12 ]; then rel="bookworm"; fi; sed -i s/stable/${rel}/g /mnt/etc/apt/sources.list;
+ late_5: sed -i '/^set -e/{n;N;d}' /mnt/etc/kernel/postinst.d/zz-update-grub
+ late_6: rm -f /usr/local/bin/dpkg-query
+ late_7: rm -f /usr/local/bin/netplan
+
diff --git a/debian/debian-cloudimg.pkr.hcl b/debian/debian-cloudimg.pkr.hcl
new file mode 100644
index 00000000..61e7ea35
--- /dev/null
+++ b/debian/debian-cloudimg.pkr.hcl
@@ -0,0 +1,125 @@
+locals {
+ qemu_arch = {
+ "amd64" = "x86_64"
+ "arm64" = "aarch64"
+ }
+ uefi_imp = {
+ "amd64" = "OVMF"
+ "arm64" = "AAVMF"
+ }
+ qemu_machine = {
+ "amd64" = "accel=kvm"
+ "arm64" = "virt"
+ }
+ qemu_cpu = {
+ "amd64" = "host"
+ "arm64" = "cortex-a57"
+ }
+
+ proxy_env = [
+ "http_proxy=${var.http_proxy}",
+ "https_proxy=${var.https_proxy}",
+ "no_proxy=${var.https_proxy}",
+ ]
+}
+
+source "null" "dependencies" {
+ communicator = "none"
+}
+
+source "qemu" "cloudimg" {
+ boot_wait = "2s"
+ cpus = 2
+ disk_image = true
+ disk_size = "4G"
+ format = "qcow2"
+ headless = var.headless
+ http_directory = var.http_directory
+ iso_checksum = "file:https://cloud.debian.org/images/cloud/${var.debian_series}/latest/SHA512SUMS"
+ iso_url = "https://cloud.debian.org/images/cloud/${var.debian_series}/latest/debian-${var.debian_version}-generic-${var.architecture}.qcow2"
+ memory = 2048
+ qemu_binary = "qemu-system-${lookup(local.qemu_arch, var.architecture, "")}"
+ qemu_img_args {
+ create = ["-F", "qcow2"]
+ }
+ qemuargs = [
+ ["-machine", "${lookup(local.qemu_machine, var.architecture, "")}"],
+ ["-cpu", "${lookup(local.qemu_cpu, var.architecture, "")}"],
+ ["-device", "virtio-gpu-pci"],
+ ["-drive", "if=pflash,format=raw,id=ovmf_code,readonly=on,file=/usr/share/${lookup(local.uefi_imp, var.architecture, "")}/${lookup(local.uefi_imp, var.architecture, "")}_CODE.fd"],
+ ["-drive", "if=pflash,format=raw,id=ovmf_vars,file=${lookup(local.uefi_imp, var.architecture, "")}_VARS.fd"],
+ ["-drive", "file=output-cloudimg/packer-cloudimg,format=qcow2"],
+ ["-drive", "file=seeds-cloudimg.iso,format=raw"]
+ ]
+ shutdown_command = "sudo -S shutdown -P now"
+ ssh_handshake_attempts = 50
+ ssh_password = var.ssh_password
+ ssh_timeout = "15m"
+ ssh_username = var.ssh_username
+ ssh_wait_timeout = "15m"
+ use_backing_file = true
+}
+
+build {
+ name = "cloudimg.deps"
+ sources = ["source.null.dependencies"]
+
+ provisioner "shell-local" {
+ inline = [
+ "cp /usr/share/${lookup(local.uefi_imp, var.architecture, "")}/${lookup(local.uefi_imp, var.architecture, "")}_VARS.fd ${lookup(local.uefi_imp, var.architecture, "")}_VARS.fd",
+ "cloud-localds seeds-cloudimg.iso user-data-cloudimg meta-data"
+ ]
+ inline_shebang = "/bin/bash -e"
+ }
+}
+
+build {
+ name = "cloudimg.image"
+ sources = ["source.qemu.cloudimg"]
+
+ provisioner "shell" {
+ environment_vars = concat(local.proxy_env, ["DEBIAN_FRONTEND=noninteractive", "DEBIAN_VERSION=${var.debian_version}", "BOOT_MODE=${var.boot_mode}"])
+ scripts = ["${path.root}/scripts/essential-packages.sh", "${path.root}/scripts/setup-boot.sh", "${path.root}/scripts/networking.sh"]
+ }
+
+ provisioner "shell" {
+ environment_vars = concat(local.proxy_env, ["DEBIAN_FRONTEND=noninteractive"])
+ expect_disconnect = true
+ scripts = [var.customize_script]
+ }
+
+ provisioner "shell" {
+ environment_vars = [
+ "CLOUDIMG_CUSTOM_KERNEL=${var.kernel}",
+ "DEBIAN_FRONTEND=noninteractive"
+ ]
+ scripts = ["${path.root}/scripts/install-custom-kernel.sh"]
+ }
+
+ provisioner "file" {
+ destination = "/tmp/"
+ sources = ["${path.root}/scripts/curtin-hooks"]
+ }
+
+ provisioner "shell" {
+ environment_vars = ["CLOUDIMG_CUSTOM_KERNEL=${var.kernel}"]
+ scripts = ["${path.root}/scripts/setup-curtin.sh"]
+ }
+
+ provisioner "shell" {
+ environment_vars = ["DEBIAN_FRONTEND=noninteractive"]
+ scripts = ["${path.root}/scripts/cleanup.sh"]
+ }
+
+ post-processor "shell-local" {
+ inline = [
+ "IMG_FMT=qcow2",
+ "SOURCE=cloudimg",
+ "ROOT_PARTITION=1",
+ "OUTPUT=${var.filename}",
+ "source ../scripts/fuse-nbd",
+ "source ../scripts/fuse-tar-root"
+ ]
+ inline_shebang = "/bin/bash -e"
+ }
+}
diff --git a/debian/debian-cloudimg.variables.pkr.hcl b/debian/debian-cloudimg.variables.pkr.hcl
new file mode 100644
index 00000000..4f5a79fd
--- /dev/null
+++ b/debian/debian-cloudimg.variables.pkr.hcl
@@ -0,0 +1,41 @@
+variable "debian_series" {
+ type = string
+ default = "bullseye"
+ description = "The codename of the debian series to build."
+}
+
+variable "debian_version" {
+ type = string
+ default = "11"
+ description = "The version number of the debian series to build."
+}
+
+variable "boot_mode" {
+ type = string
+ default = "uefi"
+ description = "The default boot mode support baked into the image."
+}
+
+variable "filename" {
+ type = string
+ default = "debian-custom-cloudimg.tar.gz"
+ description = "The filename of the tarball to produce"
+}
+
+variable "kernel" {
+ type = string
+ default = ""
+ description = "The package name of the kernel to install. May include version string, e.g linux-image-amd64=5.10.127-2~bpo10+1"
+}
+
+variable "customize_script" {
+ type = string
+ default = "/dev/null"
+ description = "The filename of the script that will run in the VM to customize the image."
+}
+
+variable "architecture" {
+ type = string
+ default = "amd64"
+ description = "The architecture to build the image for (amd64 or arm64)"
+}
diff --git a/debian/http/.gitkeep b/debian/http/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/debian/meta-data b/debian/meta-data
new file mode 100644
index 00000000..27e68d8a
--- /dev/null
+++ b/debian/meta-data
@@ -0,0 +1,2 @@
+instance-id: iid-local01
+local-hostname: packer-debian
diff --git a/debian/scripts/cleanup.sh b/debian/scripts/cleanup.sh
new file mode 100644
index 00000000..a6f0b5f4
--- /dev/null
+++ b/debian/scripts/cleanup.sh
@@ -0,0 +1,29 @@
+#!/bin/bash -ex
+#
+# cleanup.sh - Clean up what we did to be able to build the image.
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+# Everything in /run/packer_backup should be restored.
+find /run/packer_backup
+cp --preserve -r /run/packer_backup/ /
+rm -rf /run/packer_backup
+
+# We had to allow root to ssh for the image setup. Let's try to revert that.
+sed -i s/^root:[^:]*/root:*/ /etc/shadow
+rm -r /root/.ssh
+rm -r /etc/ssh/ssh_host_*
diff --git a/debian/scripts/curtin-hooks b/debian/scripts/curtin-hooks
new file mode 100644
index 00000000..e103bb36
--- /dev/null
+++ b/debian/scripts/curtin-hooks
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# curtin-hooks - Curtin installation hooks for Ubuntu
+#
+# Copyright (C) 2022 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import os
+import shutil
+
+from curtin.commands.curthooks import builtin_curthooks
+from curtin.config import load_command_config
+from curtin.util import load_command_environment
+
+
+def configure_custom_kernel(config):
+ """Amend the curtin config to explicity specify the kernel to install.
+
+ The name of the kernel to install should already have been written to the
+ CUSTOM_KERNEL file in the same directory as this file.
+ """
+ custom_kernel_path = os.path.join(
+ os.path.dirname(__file__), "CUSTOM_KERNEL")
+ with open(custom_kernel_path, "r") as custom_kernel_file:
+ custom_kernel_package = custom_kernel_file.read().strip()
+ kernel_config = config.setdefault("kernel", {})
+ kernel_config["package"] = custom_kernel_package
+ return config
+
+
+def cleanup():
+ """Remove curtin-hooks so its as if we were never here."""
+ curtin_dir = os.path.dirname(__file__)
+ shutil.rmtree(curtin_dir)
+
+
+def main():
+ state = load_command_environment()
+ config = load_command_config(None, state)
+ target = state['target']
+
+ config = configure_custom_kernel(config)
+ builtin_curthooks(config, target, state)
+ cleanup()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/debian/scripts/curtin.sh b/debian/scripts/curtin.sh
new file mode 100644
index 00000000..c3355ad0
--- /dev/null
+++ b/debian/scripts/curtin.sh
@@ -0,0 +1,38 @@
+#!/bin/bash -ex
+#
+# curtin.sh - Move curtin scripts to final destination
+#
+# Author: Alexsander de Souza
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+export DEBIAN_FRONTEND=noninteractive
+
+apt-get install -y jq
+mkdir -p /curtin
+
+# install scripts
+for s in curtin-hooks install-custom-packages setup-bootloader; do
+ if [ -f "/tmp/$s" ]; then
+ mv "/tmp/$s" /curtin/
+ chmod 750 "/curtin/$s"
+ fi
+done
+
+# copy custom packages
+if [ -f /tmp/custom-packages.tar.gz ]; then
+ mv /tmp/custom-packages.tar.gz /curtin/
+fi
diff --git a/debian/scripts/essential-packages.sh b/debian/scripts/essential-packages.sh
new file mode 100644
index 00000000..4e0ae8bc
--- /dev/null
+++ b/debian/scripts/essential-packages.sh
@@ -0,0 +1,34 @@
+#!/bin/bash -ex
+#
+# setup-boot.sh - Set up the image after initial boot
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+export DEBIAN_FRONTEND=noninteractive
+
+# Configure apt proxy if needed.
+packer_apt_proxy_config="/etc/apt/apt.conf.d/packer-proxy.conf"
+if [ ! -z "${http_proxy}" ]; then
+ echo "Acquire::http::Proxy \"${http_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+if [ ! -z "${https_proxy}" ]; then
+ echo "Acquire::https::Proxy \"${https_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+
+ARCH=$(dpkg --print-architecture)
+
+apt-get update
+apt-get -y install lvm2 xfsprogs
diff --git a/debian/scripts/install-custom-kernel.sh b/debian/scripts/install-custom-kernel.sh
new file mode 100755
index 00000000..a1926011
--- /dev/null
+++ b/debian/scripts/install-custom-kernel.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -ex
+#
+# install-custom-kernel.sh - Install custom kernel, if specified
+#
+# Copyright (C) 2021 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+export DEBIAN_FRONTEND=noninteractive
+
+if [ -z "${CLOUDIMG_CUSTOM_KERNEL}" ]; then
+ echo "Not installing custom kernel, since none was specified."
+ exit 0
+fi
+
+echo "Installing custom kernel ${CLOUDIMG_CUSTOM_KERNEL}"
+apt-get install -y ${CLOUDIMG_CUSTOM_KERNEL}
+
+# Record the installed kernel version, so that the curtin hook knows about it.
+mkdir -p /curtin
+echo -n "${CLOUDIMG_CUSTOM_KERNEL}" > /curtin/CUSTOM_KERNEL
diff --git a/debian/scripts/install-custom-packages b/debian/scripts/install-custom-packages
new file mode 100644
index 00000000..ade9a941
--- /dev/null
+++ b/debian/scripts/install-custom-packages
@@ -0,0 +1,46 @@
+#!/bin/bash -ex
+#
+# install-custom-packages - Install custom packages
+#
+# Author: Alexsander de Souza
+#
+# Copyright (C) 2021 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+export DEBIAN_FRONTEND=noninteractive
+
+PKG_TGZ="/curtin/custom-packages.tar.gz"
+
+if [ ! -f "${PKG_TGZ}" ]; then
+ exit 0
+fi
+
+WORKDIR=$(mktemp -d)
+
+cleanup() {
+ rm -rf "${WORKDIR}"
+}
+trap cleanup EXIT
+
+echo "remove existing kernels"
+dpkg -l 'linux-image-*' 'linux-headers-*' | awk '/^ii/{print $2}' | xargs apt-get -y purge
+
+echo "install new kernel"
+tar xzvf "${PKG_TGZ}" -C "${WORKDIR}"
+DEBS=$(find "${WORKDIR}" -name '*.deb')
+apt-get install -y --no-install-recommends ${DEBS}
+apt-get install --fix-broken
+
+echo "purge unused packages"
+apt-get autoremove -y
diff --git a/debian/scripts/networking.sh b/debian/scripts/networking.sh
new file mode 100644
index 00000000..3ca4ee0c
--- /dev/null
+++ b/debian/scripts/networking.sh
@@ -0,0 +1,88 @@
+#!/bin/bash -ex
+#
+# networking.sh - Prepare image to boot with cloud-init
+#
+# Author: Alexsander de Souza
+# Author: Alan Baghumian
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+export DEBIAN_FRONTEND=noninteractive
+
+# Configure apt proxy if needed.
+packer_apt_proxy_config="/etc/apt/apt.conf.d/packer-proxy.conf"
+if [ ! -z "${http_proxy}" ]; then
+ echo "Acquire::http::Proxy \"${http_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+if [ ! -z "${https_proxy}" ]; then
+ echo "Acquire::https::Proxy \"${https_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+
+apt-get install -qy cloud-init netplan.io python3-serial
+
+cat > /etc/sysctl.d/99-cloudimg-ipv6.conf < /usr/local/bin/dpkg-query < /usr/local/bin/netplan < /etc/default/netplan
+#systemctl disable networking; systemctl mask networking
+#mv /etc/network/{interfaces,interfaces.save}
+#systemctl enable systemd-networkd
diff --git a/debian/scripts/setup-boot.sh b/debian/scripts/setup-boot.sh
new file mode 100644
index 00000000..0fcdcbaa
--- /dev/null
+++ b/debian/scripts/setup-boot.sh
@@ -0,0 +1,52 @@
+#!/bin/bash -ex
+#
+# setup-boot.sh - Set up the image after initial boot
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+export DEBIAN_FRONTEND=noninteractive
+
+# Configure apt proxy if needed.
+packer_apt_proxy_config="/etc/apt/apt.conf.d/packer-proxy.conf"
+if [ ! -z "${http_proxy}" ]; then
+ echo "Acquire::http::Proxy \"${http_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+if [ ! -z "${https_proxy}" ]; then
+ echo "Acquire::https::Proxy \"${https_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+
+ARCH=$(dpkg --print-architecture)
+
+# Reset cloud-init, so that it can run again when MAAS deploy the image.
+cloud-init clean --logs
+
+apt-get update
+if [ ${BOOT_MODE} == "uefi" ]; then
+ if [ ${ARCH} == "amd64" ]; then
+ apt-get install -y grub-cloud-${ARCH} grub-efi-${ARCH}
+ else
+ apt-get install -y grub-efi-${ARCH}-signed shim-signed grub-efi-${ARCH}
+ fi
+else
+ apt-get install -y grub-cloud-${ARCH} grub-pc
+fi
+
+# Bookworm does not include this, but curtin requires this during the installation.
+if [ ${DEBIAN_VERSION} == '12' ]; then
+ wget http://ftp.us.debian.org/debian/pool/main/e/efibootmgr/efibootmgr_15-1_${ARCH}.deb
+ dpkg -i efibootmgr_15-1_${ARCH}.deb
+ rm efibootmgr_15-1_${ARCH}.deb
+fi
diff --git a/debian/scripts/setup-bootloader b/debian/scripts/setup-bootloader
new file mode 100644
index 00000000..bce05f6b
--- /dev/null
+++ b/debian/scripts/setup-bootloader
@@ -0,0 +1,50 @@
+#!/bin/bash -ex
+#
+# setup-bootloader - Install bootloader in the boot disk
+#
+# Author: Alexsander de Souza
+#
+# Copyright (C) 2023 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+export DEBIAN_FRONTEND=noninteractive
+
+# Configure apt proxy if needed.
+packer_apt_proxy_config="/etc/apt/apt.conf.d/packer-proxy.conf"
+if [ ! -z "${http_proxy}" ]; then
+ echo "Acquire::http::Proxy \"${http_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+if [ ! -z "${https_proxy}" ]; then
+ echo "Acquire::https::Proxy \"${https_proxy}\";" >> ${packer_apt_proxy_config}
+fi
+
+ARCH=$(dpkg --print-architecture)
+
+# Clean up remnants from packer-maas vm install
+rm /var/cache/debconf/config.dat
+dpkg --configure -a
+
+apt-get update
+
+if [ -f /sys/firmware/efi/runtime ]; then
+ if [ ${ARCH} == "amd64" ]; then
+ apt-get install -y grub-cloud-${ARCH} grub-efi-${ARCH}
+ else
+ apt-get install -y grub-efi-${ARCH}-signed shim-signed grub-efi-${ARCH}
+ fi
+else
+ apt-get install -y grub-cloud-${ARCH} grub-pc
+fi
+
diff --git a/debian/scripts/setup-curtin.sh b/debian/scripts/setup-curtin.sh
new file mode 100755
index 00000000..145d8d35
--- /dev/null
+++ b/debian/scripts/setup-curtin.sh
@@ -0,0 +1,31 @@
+#!/bin/bash -ex
+#
+# cloud-img-setup-curtin.sh - Set up curtin curthooks, if needed.
+#
+# Copyright (C) 2022 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+if [[ ! -f "/curtin/CUSTOM_KERNEL" ]]; then
+ echo "Skipping curtin setup, since no custom kernel is used."
+ exit 0
+fi
+
+echo "Configuring curtin to install custom kernel"
+
+mkdir -p /curtin
+
+FILENAME=curtin-hooks
+mv "/tmp/${FILENAME}" /curtin/
+chmod 750 "/curtin/${FILENAME}"
diff --git a/debian/user-data-cloudimg b/debian/user-data-cloudimg
new file mode 100644
index 00000000..dc0e2621
--- /dev/null
+++ b/debian/user-data-cloudimg
@@ -0,0 +1,19 @@
+#cloud-config
+users:
+ - name: root
+ lock_passwd: false
+ plain_text_passwd: "debian"
+ ssh_redirect_user: false
+ssh_pwauth: True
+disable_root: false
+preserve_hostname: true
+runcmd:
+ - sed -i -e '/^[#]*PermitRootLogin/s/^.*$/PermitRootLogin yes/' /etc/ssh/sshd_config
+ - systemctl restart ssh
+bootcmd:
+ - mkdir /run/packer_backup
+ - mkdir /run/packer_backup/etc
+ - mkdir /run/packer_backup/etc/apt
+ - mkdir /run/packer_backup/etc/ssh
+ - cp --preserve /etc/apt/sources.list /run/packer_backup/etc/apt/
+ - cp --preserve /etc/ssh/sshd_config /run/packer_backup/etc/ssh/
diff --git a/debian/variables.pkr.hcl b/debian/variables.pkr.hcl
new file mode 100644
index 00000000..3b3dfc76
--- /dev/null
+++ b/debian/variables.pkr.hcl
@@ -0,0 +1,50 @@
+packer {
+ required_version = ">= 1.7.0"
+ required_plugins {
+ qemu = {
+ version = "~> 1.0"
+ source = "github.com/hashicorp/qemu"
+ }
+ }
+}
+
+variable "headless" {
+ type = bool
+ default = true
+ description = "Whether VNC viewer should not be launched."
+}
+
+variable "http_directory" {
+ type = string
+ default = "http"
+}
+
+variable "http_proxy" {
+ type = string
+ default = "${env("http_proxy")}"
+}
+
+variable "https_proxy" {
+ type = string
+ default = "${env("https_proxy")}"
+}
+
+variable "no_proxy" {
+ type = string
+ default = "${env("no_proxy")}"
+}
+
+variable "ssh_password" {
+ type = string
+ default = "debian"
+}
+
+variable "ssh_username" {
+ type = string
+ default = "root"
+}
+
+variable "ssh_debian_password" {
+ type = string
+ default = "debian"
+}