Debian 12 Minimal on Azure User Guide
Overview
This guide covers the deployment and configuration of Debian 12 Minimal on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.
Debian 12, codenamed bookworm, is the current oldstable release of Debian, receiving free security updates from the standard mirrors until 30 June 2028 through the Debian LTS project. What makes this image different from the standard Debian 12 image is the package profile. Debian publishes no minimal cloud image, but Debian itself defines the minimal system in its Policy: the required and important priority classes, exactly the set debootstrap installs by default. This image keeps that Policy defined base, the Azure provisioning stack, and their dependencies — nothing else. The result, measured at capture on this exact image: 328 packages on the standard image, 278 on this one (50 fewer), and a dpkg payload 202 MiB smaller. Debian's cloud image is already famously lean, so the trim is honest about its size: what goes is the interactive and diagnostic layer a VM managed by automation never touches — the full vim, manual page content, packet capture, terminal multiplexer and bug reporting tools — while nano, vi, man and ping remain, because Debian Policy classes them as part of the base.
The image describes itself: a manifest generated from its own package database ships at /usr/share/cloudimg/debian-12-minimal.manifest, stating exactly what is installed, what was removed and what that means in practice. Anything you miss is one apt install away from the standard Debian archives.
What's included:
- Debian 12 (bookworm) with the Minimal package profile: 278 packages, cut to Debian Policy's base system plus the Azure provisioning stack, from cloudimg's own hardened standard image
- The full Azure provisioning path, protected from the trim by construction: cloud-init, the Azure Linux Agent (waagent), OpenSSH, netplan, Debian's cloud kernel and its initramfs boot chain
- A shipped manifest and trim ledger derived from the image's own package database — the delta is measured, never asserted
- Every available security update applied at build time, with
unattended-upgradesarmed and proven against the Debian security archive - Automatic root filesystem growth at first boot on larger disks — the stock Debian mechanism, fully retained and proven below
- chrony for NTP time synchronisation
- AppArmor in enforcing mode
- LLMNR disabled: the only off box listener is SSH
- Gen2 Hyper V virtual machine support
- 24/7 cloudimg support
What's deliberately NOT included (each is one apt install away): the full vim (vi via vim-tiny and nano remain), manpages content (the man command remains), tcpdump, screen, pciutils, reportbug, bash-completion, socat, the host DNS tool, polkitd, and the libraries only they used — including libicu72, a 35 MiB Unicode library only the removed tools pulled in. The standard Debian cloud image ships no classic cron, no rsyslog and no NFS client — neither does this one.
Platform: Microsoft Azure (Gen2 Hyper V, x64)
Default user: azureuser
Security posture
This image ships with no known credential. There is no default password and no baked in SSH key:
- The root account password is locked, so no one can log in as root with a password.
PasswordAuthenticationis disabled in the SSH daemon, so every login is by key.PermitRootLoginis set toprohibit-password, so root cannot be reached with a password even if one were later set.- The only key that works is the public key you supply when you create the virtual machine. Azure injects it into
azureuserat first boot, so no two machines you launch ever share a secret. - The machine identity and the SSH host keys are regenerated uniquely on every instance, so no two machines share a host key either.
- AppArmor is enabled and enforcing, and kept by the trim deliberately — the Minimal profile never trades away the standard image's security posture.
- LLMNR is disabled. Stock Debian 12 listens off box on TCP and UDP port 5355 for LLMNR, a legacy name resolution protocol with a well known spoofing attack class; this image switches it off, leaving SSH as the only off box listener.
- The trim itself is a security feature: 50 fewer installed packages is 50 fewer things to carry vulnerabilities, patch and audit.
Prerequisites
Before deploying this image, ensure you have:
- An active Microsoft Azure subscription
- Access to the Azure Portal or Azure CLI
- An SSH key pair for Linux VM access
- Familiarity with Azure VM management
Recommended VM Size: Standard_B2s (2 vCPU, 4 GB RAM) or larger. The Minimal profile is also comfortable on Standard_B1s for light duties.
Disk size: the image ships as a 30 GiB OS disk. If you deploy onto a larger disk, the root filesystem grows itself during first boot — no manual steps (see Step 7).
Step 1: Deploy the Virtual Machine
Option A: Azure Portal
- Navigate to the Azure Marketplace and search for "Debian 12 Minimal cloudimg"
- Select the image and click Create
- Configure the basics:
- Subscription: Select your Azure subscription
- Resource Group: Create new or select existing
- Virtual Machine Name: Enter a name for your VM
- Region: Select your preferred Azure region
- Size:
Standard_B2srecommended - Under Administrator Account, select SSH public key and enter your key
- Under Inbound Port Rules, allow SSH (port 22)
- On the Disks tab, set the OS disk size you want — the root filesystem will fill it automatically at first boot
- Click Review + Create, then Create
Option B: Azure CLI
az vm create \
--resource-group myResourceGroup \
--name my-debian-minimal-vm \
--image cloudimg:debian:debian12minimal:latest \
--size Standard_B2s \
--os-disk-size-gb 64 \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
--os-disk-size-gb 64 deploys the 30 GiB image onto a 64 GiB disk. Nothing further is needed: the root partition and filesystem grow to fill the disk during first boot.
Step 2: Connect via SSH
Run these on your own workstation, not on the VM. Find the public IP, then connect:
az vm show --resource-group myResourceGroup --name my-debian-minimal-vm --show-details --query publicIps -o tsv
ssh azureuser@<vm-ip>
There is no password to enter. If SSH asks you for one, the key you supplied at create time is not the key your client is offering.
Step 3: Confirm the release and the Minimal profile
Check that the machine is the Debian 12 release you expect:
grep -E '^(NAME|VERSION|ID|VERSION_ID|PRETTY_NAME)=' /etc/os-release
cat /etc/debian_version
uname -srm
dpkg --print-architecture
Expected output:
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
ID=debian
12.15
Linux 6.1.0-53-cloud-amd64 x86_64
amd64

The -cloud-amd64 kernel matters: this image keeps Debian's cloud kernel flavour, with the Hyper V drivers included — the same kernel Debian's own Azure images run. Then confirm the defining property of this image — the Minimal package profile:
dpkg-query -Wf '${Package}\n' | sed 's/:.*$//' | sort -u | wc -l
df -h /
Expected output on a fresh VM deployed with the default 30 GiB disk:
278
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 30G 616M 28G 3% /

278 packages against the standard image's 328. The trim was performed as a single guarded, simulated first package transaction down to Debian Policy's own base system definition, and the provisioning path (cloud-init, the Azure agent, OpenSSH, netplan, the boot chain) is on the guard's protected list — it cannot be trimmed by construction.
Step 4: Read the shipped manifest
The image states what it is. The manifest is generated from the package database at capture — never written by hand — so it cannot disagree with the machine you are on:
head -10 /usr/share/cloudimg/debian-12-minimal.manifest
Expected output:
# Debian 12 Minimal — cloudimg shipped manifest
# DERIVED from the dpkg database on this image at 2026-09-12T09:55:31Z (capture time) — never hand-written.
# Baseline: the cloudimg 'Debian 12' (std) image this Minimal is cut from.
#
# WHAT 'MINIMAL' MEANS HERE: Debian publishes no minimal cloud artefact, but
# Debian Policy defines the minimal system — priority classes required +
# important, exactly what debootstrap installs by default. This image keeps
# that Policy-defined base, the Azure provisioning/boot/security lineage
# (cloud-init, the Azure agent, kernel + boot chain, networking, chrony,
# AppArmor, unattended-upgrades), and their dependencies — nothing else.
The capability notes further down tell you, in plain terms, what is and is not on board and how to restore each piece:
sed -n '/capability notes/,/^#$/p' /usr/share/cloudimg/debian-12-minimal.manifest
Expected output:
# --- capability notes, each DERIVED from a probe on this image ---
NFS: this image has NO NFS client (the std Debian azure image ships none either),
NFS: so 'mount -t nfs' will not work as shipped. Restore it with: sudo apt install nfs-common
MAN: the man command is installed (man-db is priority important — part of the Policy-defined base).
CRON: no classic cron daemon (the std Debian azure image ships none either); systemd
CRON: timers provide scheduling. Restore classic cron with: sudo apt install cron
EDITOR: nano and vi (vim-tiny) are installed (both priority important — Policy base);
EDITOR: full vim was removed by the trim. Restore with: sudo apt install vim
PROVISIONING: cloud-init 22.4.2 + waagent 2.7.3.0-4.1 are installed — the full Azure provisioning path is retained.
KERNEL: 6.1.0-53-cloud-amd64 (Debian's cloud kernel flavour — Hyper-V drivers included, the same kernel the std image runs).
UPDATES: unattended-upgrades is installed and armed — security updates auto-apply, exactly as on the std image.
GROW: the root filesystem auto-grows to the disk size you deploy (the stock Debian
GROW: mechanism — initramfs growroot + x-systemd.growfs + cloud-init — is fully retained).
DOCS: the trim is package-level; /usr/share/doc of every RETAINED package is intact.

The full installed package list follows the notes in the same file, one package per line with its version — useful for compliance snapshots and drift comparisons.
Step 5: Check the patch level
The image is fully updated at build time. Confirm that no upgrades are pending and that the update path is intact:
apt-get -s -o APT::Get::Always-Include-Phased-Updates=true dist-upgrade | grep -c '^Inst ' | sed 's/^/pending upgrades: /'
apt-mark showhold
dpkg -l | awk '/^ii linux-image-[0-9]/{print $2, $3}'
Expected output shows pending upgrades: 0, no held packages, and exactly one installed cloud kernel:
pending upgrades: 0
linux-image-6.1.0-53-cloud-amd64 6.1.187-1
Debian has no phased updates in practice, but the flag is accepted and harmless, and it keeps this check identical across the cloudimg Debian and Ubuntu families. Debian 12 receives security updates from the standard mirrors, free of charge, until 30 June 2028. This image also arms unattended-upgrades, proven at build time with a real dry run resolving the Debian-Security origin — so security updates keep applying themselves. You can still run updates on your own schedule:
sudo apt-get update && sudo apt-get -y dist-upgrade
Reboot if a new kernel was installed:
sudo systemctl reboot
Step 6: Verify the security posture
Confirm AppArmor is enforcing, root is locked, SSH is key only, and LLMNR is off:
sudo aa-status --enabled && echo "apparmor: enabled"
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '
resolvectl status 2>/dev/null | awk '/-LLMNR/ && !p {print; p=1}'
Expected output:
apparmor: enabled
root L 2009-12-22 -1 -1 -1 -1
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

without-password is how OpenSSH reports the prohibit-password setting. Both names mean the same thing: root may not authenticate with a password. The L from passwd -S means the root account is locked, and -LLMNR confirms the legacy multicast name resolution listener is off.
Step 7: Disk growth — automatic on this image
If you deployed onto a disk larger than 30 GiB (as in the Step 1 CLI example), there is nothing to do: this image keeps the standard flat root layout and the stock Debian growth mechanism. The root partition is extended in the initramfs during first boot, and systemd grows the filesystem as it mounts. Confirm it:
df -h /
Expected output on a 64 GiB disk, exactly as produced by this image:
Filesystem Size Used Avail Use% Mounted on
/dev/sdb1 63G 616M 60G 2% /
The proof is in the kernel's own log — the ext4 online resize that systemd-growfs performed at mount time:
sudo dmesg | grep 'resized filesystem'
grep 'NOCHANGE\|resized' /var/log/cloud-init.log | tail -1
Expected output (block counts vary with your disk size):
[ 10.490614] EXT4-fs (sdb1): resized filesystem to 16744443
2026-09-12 10:06:44,512 - cc_growpart.py[DEBUG]: '/' NOCHANGE: no change necessary (/dev/sdb, 1)
Azure enumerates the OS disk as sda or sdb depending on the boot — the root filesystem is the same either way. The NOCHANGE line is correct and expected on Debian: by the time cloud-init's growpart module looks at the disk, the initramfs has already grown the partition — cloud-init finds nothing left to do. (On distributions without the initramfs grow step, the same line reads resized: changed.)
Growing an existing VM later: enlarge the OS disk (deallocate the VM, az disk update --resource-group <rg> --name <os-disk-name> --size-gb <new-size>, start the VM — or use the portal's Disks blade). The root grows again on the next boot; no manual partitioning is ever needed on this layout.
Step 8: Networking and the firewall
The Azure Network Security Group is the control plane for inbound and outbound traffic, and it is the first and usually the only place you need to open a port. The image advertises a single inbound port, SSH on 22. Confirm what is listening:
sudo ss -tlnp
Expected output — sshd on port 22 (IPv4 and IPv6), and systemd-resolved on the loopback-only stub addresses:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=429,fd=14))
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=429,fd=16))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=635,fd=3))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=635,fd=4))
On stock Debian 12 you would also see 0.0.0.0:5355 and [::]:5355 — systemd-resolved's LLMNR listener. This image disables LLMNR (it is a spoofing vector, deprecated, and useless on an Azure VNet), so SSH is the only off box socket.
Debian ships no host firewall enabled by default — nftables is the framework and the NSG is the enforced firewall, which is the normal arrangement on Azure. If your policy requires a host firewall as well, install and enable one, adding the SSH rule first so you do not lock yourself out:
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw enable
Once ufw is running, a port must be open in both the Azure NSG and ufw before traffic reaches your service.
Step 9: Install software
The trim removes nothing from your reach: the machine points at the standard Debian archives (main component), and every removed package — and anything else in the archive — installs normally. On a freshly booted VM the package metadata cache may still be empty (the cleanup that prepares the image clears it, and the apt timers refresh it within minutes) — if the search below prints nothing, run sudo apt-get update once first. Search, then install what you need:
apt-cache search --names-only '^nginx' 2>/dev/null | sed -n '1,3p'
Expected output:
nginx - small, powerful, scalable web/proxy server
nginx-common - small, powerful, scalable web/proxy server - common files
nginx-core - nginx web/proxy server (standard version)
Install a package and enable it in the usual way, for example sudo apt-get install -y nginx followed by sudo systemctl enable --now nginx. The everyday absences and their one-line restores are in the shipped manifest (Step 4): vim for the full editor, cron for classic cron jobs, nfs-common for NFS mounts, tcpdump for packet capture.
To add another administrator, create the account with sudo adduser <name>, add it to the sudo group with sudo usermod -aG sudo <name>, then copy your public key into /home/<name>/.ssh/authorized_keys with mode 600 and the .ssh directory mode 700, owned by that user. The image ships with no human accounts at all, so azureuser is the only login until you add one:
getent group sudo
Expected output:
sudo:x:27:azureuser
Common Use Cases
- Container and automation hosts that want the smallest practical Debian base under their workloads
- Security conscious deployments: fewer installed packages to patch, scan and audit
- Fleets managed entirely by SSH and configuration management, where interactive niceties are dead weight
- A fast booting, quick patching stable Debian base for ephemeral and scale out workloads
Troubleshooting
Cannot connect via SSH
- Verify the VM is in Running state in the Azure Portal
- Check that port 22 is allowed in the Network Security Group
- Ensure you are using the correct username:
azureuser - Verify your SSH key matches the one you supplied at create time. Password login is disabled by design, so a wrong key cannot fall back to a password prompt.
To see which key your client is actually offering, run ssh -v azureuser@<vm-ip> from your workstation and look for the Offering public key lines.
"command not found" for vim, crontab or tcpdump
Expected — those tools are outside Debian Policy's base system, which this image is cut to. Each is one command away (see the shipped manifest, Step 4): sudo apt install vim, sudo apt install cron, sudo apt install tcpdump. Until then, vi (vim-tiny) and nano are both present — Debian Policy classes them as part of the base, so the trim keeps them.
df shows 30G on a bigger disk
The automatic grow runs during first boot. If you resized the disk while the VM was running, the VM has not rebooted since the disk grew — reboot once (sudo systemctl reboot) and the space is claimed. Check the evidence trail: sudo dmesg | grep 'resized filesystem' shows the ext4 online resize, and journalctl -u 'systemd-growfs@-.service' shows the unit that performed it (Successfully resized "/" to 63.8G bytes on a 64 GiB disk).
Azure agent not running
sudo systemctl status walinuxagent
sudo systemctl enable --now walinuxagent
The Debian package is named waagent; the systemd unit it ships is walinuxagent.service.
Package manager issues
Refresh the metadata cache first:
sudo apt-get update
If repositories are unreachable, check DNS resolution. getent is always present and resolves through the same system resolver:
getent hosts deb.debian.org
A service is blocked and the logs mention AppArmor
AppArmor is enforcing on this image. A healthy machine has no denials — measured on this exact build:
sudo journalctl -k --no-pager | awk '/apparmor/ && /DENIED/{n++} END{print "apparmor denials: " n+0}'
Expected output:
apparmor denials: 0
If you see denials after installing software, the denied profile's name appears in the log line — adjust or disable that one profile rather than turning AppArmor off.
Important Notes
Debian is free and open source. It is not covered by a single licence: it is a distribution assembled from thousands of independently packaged components, each carrying its own terms, including the GPL, LGPL, MIT, BSD and Apache 2.0. Every package's terms are readable with dpkg -s <package> and under /usr/share/doc/<package>/copyright. Everything in this image comes from Debian's DFSG free main component. No subscription or licence key is required to run this image.
Debian 12 is the current oldstable release. It continues to receive security updates through the Debian LTS project, published to the standard mirrors free of charge, until 30 June 2028 — no subscription and no configuration change needed. Plan a move to a newer stable image, such as the cloudimg Debian 13 family, as that date approaches.
This image's Minimal profile is package level: the installed set is Debian Policy's base system plus the Azure provisioning stack, while documentation belonging to the packages that remain is kept on disk. The shipped manifest and trim ledger at /usr/share/cloudimg/ record the exact measured delta against the standard image, including which removals are part of Debian's own published cloud profile.
Debian is a registered trademark of Software in the Public Interest, Inc. (SPI), used here nominatively, only to identify the distribution this image contains. cloudimg is not affiliated with, endorsed by or sponsored by the Debian Project or SPI.
Support
For assistance with this image, contact cloudimg support:
- Website: www.cloudimg.co.uk
- Product Catalogue: www.cloudimg.co.uk/products
- User Guides: www.cloudimg.co.uk/guides
- SLA: 24 hour response guaranteed