Operating Systems Azure

Ubuntu 20.04 LTS Minimal on Azure User Guide

| Product: Ubuntu 20.04 LTS Minimal on Azure

Overview

This guide covers the deployment and configuration of Ubuntu Server 20.04 LTS Minimal on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.

Ubuntu is a widely used Linux distribution produced by Canonical. Version 20.04 (Focal Fossa) is a long term support (LTS) release whose standard security maintenance has ended — read the notice below before choosing this image. What makes this image different from the standard Ubuntu Server 20.04 LTS image is the package profile: the installed set is trimmed to Canonical's own published Ubuntu Minimal cloud image seed — the package set Canonical themselves define as Minimal for this release — plus the Azure provisioning stack. The result, measured at capture on this exact image: 617 packages on the standard image, 408 on this one (209 fewer), and a dpkg payload 226 MiB smaller. Fewer installed packages means a smaller attack surface, fewer packages to patch and scan, and faster update runs.

The image describes itself: a manifest generated from its own package database ships at /usr/share/cloudimg/ubuntu-20-04-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 Ubuntu archives.

End of standard support notice — read this first

Ubuntu 20.04 LTS reached the end of its standard support on 31 May 2025. Two facts follow, and both matter:

  • This image is patched to the last packages published in the free Ubuntu archive for 20.04, as of the date the image was captured. The focal archive is still online and apt keeps working against it, but Canonical publishes no further free security updates for 20.04 — over time the machine will carry vulnerabilities that the free archive can never fix.
  • Continued security coverage for 20.04 exists, but only through Ubuntu Pro, Canonical's paid subscription, whose Expanded Security Maintenance (ESM) stream covers 20.04 into 2030. ESM packages are delivered only to machines attached to an Ubuntu Pro subscription and are contractually non redistributable, so this image contains no ESM package and no Ubuntu Pro attachment. If your workload needs ongoing security updates, attach your own subscription with sudo pro attach <token> after deployment — or choose the Ubuntu 22.04 LTS Minimal or Ubuntu 24.04 LTS Minimal image, both of which are inside their free standard maintenance windows on the same Minimal package profile.

Choose this image when your estate is standardised on 20.04 — a dependency pinned to focal, a fleet mid migration, a vendor appliance certified against it — and you want the smallest practical footprint underneath it. For a fresh deployment with no 20.04 constraint, use a newer LTS.

What's included:

  • Ubuntu Server 20.04 LTS with the Minimal package profile: 408 packages, cut to Canonical's published Minimal seed from cloudimg's own hardened standard image
  • The full Azure provisioning path, protected from the trim by construction: cloud-init, the Azure Linux Agent (walinuxagent), OpenSSH, netplan, the Azure kernel flavour 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
  • Patched to the final free archive state for 20.04 at build time, with no ESM package and no Ubuntu Pro attachment
  • unattended-upgrades armed — the timers still run against the free archive exactly as on a supported release, and start delivering ESM the moment you attach your own subscription
  • Automatic root filesystem growth at first boot on larger disks (cloud-init growpart, proven below)
  • AppArmor in enforcing mode
  • Gen2 Hyper V virtual machine support
  • 24/7 cloudimg support

What's deliberately NOT included (matching Canonical's Minimal seed — each is one apt install away): the man command, full screen editors (vim, nano), ping and network diagnostic tools, classic cron (systemd timers remain), rsyslog (journald remains), an NFS client, ufw, and a long tail of libraries and utilities a cloud VM managed by automation never touches.

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.
  • PasswordAuthentication is disabled in the SSH daemon, so every login is by key.
  • PermitRootLogin is set to prohibit-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 azureuser at 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 left enabled and enforcing, the Ubuntu default.
  • The trim itself is a security feature: 209 fewer installed packages is 209 fewer things to carry vulnerabilities, patch and audit. On a release past its free maintenance window that argument carries more weight than usual — every package removed is one that can no longer need a fix you cannot get for free.

Prerequisites

Before deploying this image, ensure you have:

  1. An active Microsoft Azure subscription
  2. Access to the Azure Portal or Azure CLI
  3. An SSH key pair for Linux VM access
  4. 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 at first boot — no manual steps (see Step 7).

Step 1: Deploy the Virtual Machine

Option A: Azure Portal

  1. Navigate to the Azure Marketplace and search for "Ubuntu 20.04 LTS Minimal cloudimg"
  2. Select the image and click Create
  3. Configure the basics:
  4. Subscription: Select your Azure subscription
  5. Resource Group: Create new or select existing
  6. Virtual Machine Name: Enter a name for your VM
  7. Region: Select your preferred Azure region
  8. Size: Standard_B2s recommended
  9. Under Administrator Account, select SSH public key and enter your key
  10. Under Inbound Port Rules, allow SSH (port 22)
  11. On the Disks tab, set the OS disk size you want — the root filesystem will fill it automatically at first boot
  12. Click Review + Create, then Create

Option B: Azure CLI

az vm create \
  --resource-group myResourceGroup \
  --name my-ubuntu-minimal-vm \
  --image cloudimg:ubuntu-20-04-minimal:ubuntu2004minimal: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: cloud-init grows the partition and the filesystem 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-ubuntu-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 Ubuntu 20.04 LTS release you expect:

grep -E '^(NAME|VERSION|ID|VERSION_ID|PRETTY_NAME)=' /etc/os-release
uname -srm
dpkg --print-architecture

Expected output:

NAME="Ubuntu"
VERSION="20.04.6 LTS (Focal Fossa)"
ID=ubuntu
PRETTY_NAME="Ubuntu 20.04.6 LTS"
VERSION_ID="20.04"
Linux 5.15.0-1089-azure x86_64
amd64

The terminal shows the os-release fields reporting NAME Ubuntu, VERSION 20.04.6 LTS Focal Fossa, ID ubuntu and VERSION_ID 20.04, uname reporting an x86_64 azure kernel, and dpkg print-architecture reporting amd64

The -azure kernel matters: this image keeps the Azure kernel flavour of the standard image, with the Hyper V drivers built in — the same arrangement Canonical use for their own Azure images. 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:

408
Filesystem      Size  Used Avail Use% Mounted on
/dev/root        29G  1.6G   28G   6% /

The terminal shows the installed package count of 408, the measured trim of 209 packages against the standard image from the shipped ledger with the largest removals listed, and df reporting a lean root filesystem

408 packages against the standard image's 617. The trim was performed as a single guarded, simulated first package transaction against Canonical's published Minimal manifest, 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 -8 /usr/share/cloudimg/ubuntu-20-04-minimal.manifest

Expected output:

# Ubuntu 20.04 LTS Minimal — cloudimg shipped manifest
# DERIVED from the dpkg database on this image at 2026-09-12T15:01:35Z (capture time) — never hand-written.
# Baseline: the cloudimg 'Ubuntu Server 20.04 LTS' (std) image this Minimal is cut from.
#
# Measured trim delta vs the std baseline: 617 -> 408 packages (-209), dpkg payload -226 MiB.
# Trim target: Canonical's published Ubuntu 20.04 Minimal cloud-image package seed (manifest sha256 b5831b118affb528...).
# Guard refusals (candidates KEPT because their removal would cascade a keeper): libdw1 libip6tc2 libnetfilter-conntrack3 libnfnetlink0 libnftnl11 libnuma1 libpci3 libslang2 libunwind8 pci.ids
# Installed now: 408 packages, dpkg payload 790 MiB, rootfs used 1599 MiB.

The capability notes further down tell you, in plain terms, what is not on board, what the end of standard support means for this machine, and how to restore each piece:

sed -n '/capability notes/,/^#$/p' /usr/share/cloudimg/ubuntu-20-04-minimal.manifest

Expected output:

# --- capability notes, each DERIVED from a probe on this image ---
NFS: this image has NO NFS client. nfs-common is not part of the Ubuntu Minimal seed,
NFS: so 'mount -t nfs' will not work as shipped. Restore it with: sudo apt install nfs-common
MAN: no man command (man-db is not part of the Minimal seed). Restore with: sudo apt install man-db
CRON: no classic cron daemon (cron is not part of the Minimal seed); systemd timers
CRON: provide scheduling. Restore classic cron with: sudo apt install cron
EDITOR: no full-screen editor ships (matching Canonical's Minimal seed).
EDITOR: Install one with: sudo apt install nano   (or vim)
PROVISIONING: cloud-init 24.4.1 + walinuxagent 2.2.46 are installed — the full Azure provisioning path is retained.
KERNEL: 5.15.0-1089-azure (the Azure kernel flavour — Hyper-V drivers built in, matching Canonical's own Azure minimal SKUs).
UPDATES: unattended-upgrades is installed and armed — security updates auto-apply, exactly as on the std image.
SNAPS: snapd is part of Canonical's Minimal seed and is retained; the base image's seeded snaps are
SNAPS: retained too (the trim is dpkg-level): core20 lxd snapd.
SNAPS: Remove any you do not need with: sudo snap remove <name>
EOL: Ubuntu 20.04 LTS reached END OF STANDARD SUPPORT on 31 May 2025. Canonical publishes no further
EOL: free security updates for this release. This image is patched to the FINAL FREE ARCHIVE state
EOL: (azure.archive.ubuntu.com focal / focal-updates / focal-security), measured 0 upgradable at capture.
EOL: ESM/Ubuntu Pro is NOT included and NOT attached (machine token present: no; installed packages
EOL: carrying a '+esm' version: 0). ESM packages are non-redistributable, so they can never ship
EOL: inside a Marketplace image. To continue receiving Canonical security updates you may attach your own
EOL: Ubuntu Pro subscription after deployment: sudo pro attach <your-token>
DOCS: unlike Canonical's minimal cloud image, this image does NOT strip documentation of
DOCS: RETAINED packages at the dpkg level — the trim is package-level; /usr/share/doc of the kept set is intact.

The terminal shows the shipped manifest header with the measured 209-package trim against the standard baseline and the capability notes covering NFS, man, cron, editor, provisioning, kernel, updates, snaps and the end of standard support posture, each derived from a probe on the image itself

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 against the free archive 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 Azure kernel:

pending upgrades: 0
linux-image-5.15.0-1089-azure 5.15.0-1089.98~20.04.1

On a release past its end of standard support, pending upgrades: 0 means something narrower than it does on a supported release: the machine has everything the free focal archive will ever publish. It does not mean the machine is patched against vulnerabilities disclosed since — fixes for those exist only in Canonical's paid ESM stream (see the notice at the top of this guide). The Always-Include-Phased-Updates=true flag matters on Ubuntu: without it, a plain check can silently hide phased updates held back on this particular machine.

The image still arms unattended-upgrades — which is part of Canonical's Minimal seed, so the automatic update path survives the trim by design — with the security origin enabled, so if you attach an Ubuntu Pro subscription the ESM fixes start flowing automatically through the same mechanism; without one the timers simply find nothing new. You can still run updates on your own schedule:

sudo apt-get update && sudo apt-get -y dist-upgrade

To attach an Ubuntu Pro subscription you have purchased (or a free personal token) and start receiving ESM updates:

sudo pro attach <your-token>

Reboot if a new kernel was installed:

sudo systemctl reboot

Step 6: Verify the security posture

Confirm AppArmor is enforcing, root is locked, and SSH is key only:

sudo aa-status --enabled && echo "apparmor: enabled"
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) '

Expected output:

apparmor: enabled
root L 12/22/2009 -1 -1 -1 -1
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no

The terminal shows aa-status reporting AppArmor enabled, passwd -S root reporting the root account as L meaning locked, and the effective sshd policy reporting permitrootlogin without-password, pubkeyauthentication yes and passwordauthentication no

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.

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 cloud-init's growpart module extends the root partition and filesystem during first boot. Confirm it:

df -h /
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT "$(readlink -f /dev/disk/azure/root)"

Expected output on a 64 GiB disk, exactly as produced by this image (on 20.04 the root partition is sda1 and /boot lives on the root filesystem — there is no separate boot partition):

Filesystem      Size  Used Avail Use% Mounted on
/dev/root        62G  1.3G   61G   2% /
NAME     SIZE TYPE MOUNTPOINT
sda       64G disk
|-sda1  63.9G part /
|-sda14    4M part
`-sda15  106M part /boot/efi

The proof of the automatic grow is in cloud-init's own log:

sudo grep 'resized' /var/log/cloud-init.log | tail -1

Expected output (byte counts vary with your disk size):

2026-09-12 15:31:49,301 - cc_growpart.py[INFO]: '/' resized: changed (/dev/disk/by-partuuid/e6bff0e0-aeb0-43e4-a42d-6cc823575c69) from 32096894464 to 68603067904

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). cloud-init grows the root 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 address:

State     Recv-Q    Send-Q       Local Address:Port        Peer Address:Port    Process
LISTEN    0         128                0.0.0.0:22               0.0.0.0:*        users:(("sshd",pid=804,fd=3))
LISTEN    0         4096         127.0.0.53%lo:53               0.0.0.0:*        users:(("systemd-resolve",pid=503,fd=13))
LISTEN    0         128                   [::]:22                  [::]:*        users:(("sshd",pid=804,fd=4))

ufw is not installed on this image — it is not part of Canonical's Minimal seed. 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. (The iptables layer itself is present — the Azure agent uses it to protect the platform metadata endpoint.)

Step 9: Install software

The trim removes nothing from your reach: the machine points at the standard Ubuntu archives, and every removed package — and anything else the free focal archive still carries — installs normally. 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. Remember that on a release past its end of standard support, anything you install from the free archive is itself frozen at its final free version. The everyday absences and their one-line restores are in the shipped manifest (Step 4): man-db for manual pages, nano or vim for an editor, iputils-ping for ping, cron for classic cron jobs, nfs-common for NFS mounts.

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

  • Estates pinned to 20.04 that want the smallest practical footprint under a dependency they cannot move yet
  • Container and automation hosts that want a lean Ubuntu 20.04 base under their workloads
  • Security conscious deployments on a frozen release: fewer installed packages to patch, scan and audit
  • Fleets managed entirely by SSH and configuration management, where interactive niceties are dead weight

Troubleshooting

Cannot connect via SSH

  1. Verify the VM is in Running state in the Azure Portal
  2. Check that port 22 is allowed in the Network Security Group
  3. Ensure you are using the correct username: azureuser
  4. 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 man, vim, nano, ping or crontab

Expected — those tools are not part of Canonical's Minimal seed this image is cut to. Each is one command away (see the shipped manifest, Step 4): sudo apt install man-db, sudo apt install nano, sudo apt install iputils-ping, sudo apt install cron. Until an editor is installed, remember that sed -i and here-documents cover most quick edits, and systemctl edit opens its own fallback editor.

df shows 29G 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 cloud-init claims the space. Check the evidence trail in /var/log/cloud-init.log: a cc_growpart line reading '/' resized: changed means the grow ran; '/' NOCHANGE: no change necessary means the partition already fills the disk.

Azure agent not running

sudo systemctl status walinuxagent
sudo systemctl enable --now walinuxagent

Package manager issues

Refresh the metadata cache first:

sudo apt-get update

The focal package archive remains online after the end of standard support, so apt-get update succeeding against it is the expected state — it just no longer receives new security uploads outside the paid ESM stream. If repositories are unreachable, check DNS resolution. getent is always present and resolves through the same system resolver:

getent hosts archive.ubuntu.com

A service is blocked and the logs mention AppArmor

AppArmor is enforcing on this image. A healthy machine shows zero denials — measured zero on a fresh instance of this exact build. The check below excludes cloud-init's provisioning DHCP probe, which runs dhclient and is denied on some 20.04 boots without any effect on provisioning:

sudo journalctl -k --no-pager | awk '/apparmor/ && /DENIED/ && $0 !~ /sbin\/dhclient/{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

Ubuntu 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, Apache 2.0 and MPL. Every package's terms are readable with dpkg -s <package> and under /usr/share/doc/<package>/copyright. No subscription or licence key is required to run this image; Ubuntu Pro is an optional Canonical subscription that is neither included nor required here — though, as the notice at the top of this guide explains, after 20.04's end of standard support on 31 May 2025 it is the only route to further Canonical security updates, and its ESM packages are non redistributable, so this image ships without them and is patched to the final free archive state instead.

This image's Minimal profile is package level: the installed set matches Canonical's published Minimal cloud image seed (plus the Azure provisioning stack), while documentation belonging to the packages that remain is kept on disk, and the base image's seeded snaps are kept too. Canonical's own minimal cloud image additionally strips documentation files at the dpkg level; if you want that behaviour you are better served installing their image outside the Marketplace — this image chooses the less surprising default.

Ubuntu is a trademark of Canonical Ltd. The name is used here nominatively, only to identify the distribution this image contains. cloudimg is not affiliated with, endorsed by or sponsored by Canonical Ltd.

Support

For assistance with this image, contact cloudimg support: