Ubuntu autoinstall is a YAML answer file that drives the Subiquity installer with nobody at the keyboard. It replaced debian-installer preseeding on Ubuntu Server 20.04 and reached Desktop in 23.04. Hand it to the installer through cloud-init (a CIDATA volume or an HTTP URL) or as autoinstall.yaml on the media, and it partitions, installs and reboots alone.
cloud-init is the other half. It delivers the autoinstall file to the installer, and a nested user-data: section is passed through to cloud-init again on the installed system's first boot. Get that split right, test every change in a VM, keep passwords and tokens out of the file, and pin what you install, and the same config will build the hundredth machine exactly like the first.
This guide goes one level deeper than our walkthroughs on writing an OS ISO to USB and verifying it and installing Ubuntu next to Windows or on a Mac. Those cover a person clicking through the installer. This one covers the case where nobody clicks.
Key takeaways
- Autoinstall is not preseed. It is YAML, it is read by Subiquity (the Ubuntu installer), and any question you do not answer takes the default instead of stopping to ask. The Subiquity documentation lists support from Ubuntu Server 20.04 and Ubuntu Desktop 23.04 onward.
- Two layers, two timings. Keys under
autoinstall:configure the target system during installation. Keys underautoinstall: user-data:are cloud-init directives that run on the installed system's first boot. Plain#cloud-configkeys at the top level only touch the temporary live installer environment. - Three delivery routes. A cloud-init NoCloud seed (a volume labelled
CIDATAor an HTTP URL on the kernel command line) is the route the docs recommend. Anautoinstall.yamlat the root of the install media also works. The kernel command linesubiquity.autoinstallpath=parameter wins over everything else. - The installer asks before wiping a disk unless
autoinstallis on the kernel command line. Keep that prompt for USB sticks that might be plugged into the wrong machine. - Secrets end up in places you do not expect. The password is a hash, but the LUKS passphrase in the storage layout is plain text, and the config file is copied around by cloud-init and the installer. Serve configs from short lived endpoints, use temporary credentials, and rotate them on first boot.
- Validate before you boot. Subiquity ships
validate-autoinstall-user-data.py, and cloud-init shipscloud-init schema. Both catch the common mistakes: a missing#cloud-configheader, a misspelledautoinstall:key, a string where a list belongs. - Reproducible means pinned. Use
package=versionin the packages list, a fixed kernel package, a fixed snap channel, an internal or snapshot mirror, and capture a package manifest duringlate-commandsso you can turn it into an SBOM.
What replaced debian-installer preseed, and since which release?
For most of Ubuntu's history, unattended installs used preseeding: a file of debconf-set-selections answers fed to debian-installer (d-i). Ubuntu Server moved to a new installer, Subiquity, and a new answer format called autoinstall. The Ubuntu 20.04 LTS release notes describe the live server installer as "now the preferred media to install Ubuntu Server on all architectures" and list "support for automated installs" as one of its main new features. The Subiquity documentation states the autoinstall format is supported in Ubuntu Server 20.04 and later, and Ubuntu Desktop 23.04 and later.
The Desktop side moved later. The Ubuntu 23.04 release notes say the default Desktop installer became "a Flutter app backed by subiquity and packaged as a snap", with the legacy installer still available that cycle. Ubuntu 24.04 LTS took it further: the release notes describe the Desktop installer becoming part of a larger provisioning project and adding a page that can side load an autoinstall.yaml from a network URL during installation.
Timeline
| Date | Release or component | What changed for unattended installs | Source |
|---|---|---|---|
| 23 Apr 2020 | Ubuntu 20.04 LTS | Live server installer becomes the preferred Server media; automated installs supported | Ubuntu 20.04 release notes; Subiquity intro |
| 20 Apr 2023 | Ubuntu 23.04 | Desktop installer becomes a Subiquity backed app; autoinstall supported on Desktop from this release | Ubuntu 23.04 release notes; Subiquity intro |
| 12 Oct 2023 | Ubuntu 23.10 | TPM-backed full disk encryption introduced as an experimental Desktop feature | Ubuntu 24.04 release notes |
| 25 Apr 2024 | Ubuntu 24.04 LTS | A single top level autoinstall: key accepted on install media; stricter top level validation; Desktop can side load autoinstall.yaml from a URL; cloud-init 24.1.3 with schema validation of user-data, vendor-data and network-config | Subiquity docs; Ubuntu 24.04 release notes |
| 2024 | Subiquity 24.08.1 | Disk match specs can be an ordered list (try serial first, then path) | Autoinstall reference |
| 10 Oct 2024 | Ubuntu 24.10 | kernel-crash-dumps defaults to dynamic enablement when hardware qualifies | Autoinstall reference |
| 23 Apr 2026 | Ubuntu 26.04 LTS | Current LTS used in the official quick start | Ubuntu list of releases; autoinstall quick start |
Preseed compared with autoinstall
| Behaviour | debian-installer preseed | Subiquity autoinstall |
|---|---|---|
| File format | debconf-set-selections lines | YAML, validated against a JSON schema |
| Unanswered question | Installer stops and asks | Installer takes the default (fails if there is no default) |
| Mixed mode | Partial preseed, then prompts | interactive-sections lists screens to still show |
| Storage | Partman recipes | Named layouts (lvm, direct, zfs, hybrid) or a curtin style action list |
| Post install hooks | preseed/late_command with in-target | late-commands with curtin in-target |
| First boot config | Separate tooling | Nested cloud-init user-data |
| Error handling | Varies | error-commands run, traceback printed, installer waits |
| Debconf answers | Native | Still possible through debconf-selections |
Two consequences matter in practice. First, an autoinstall file that answers almost nothing still produces a full install, because every gap is filled with a default, including "install to the largest disk". Second, you cannot drop an old preseed file onto a new ISO and expect anything to happen; it needs rewriting.
How autoinstall and cloud-init fit together
The Subiquity docs put it plainly: cloud-init may provide the autoinstall configuration to the installer, but "it does not process the autoinstall directives itself". There are three places a key can sit, and each runs at a different time on a different system.
#cloud-config
# 1. Top level cloud-config keys: applied to the LIVE installer
# environment only, and gone after reboot.
autoinstall:
# 2. Autoinstall directives: read by Subiquity, used to build
# the TARGET system during installation.
version: 1
user-data:
# 3. cloud-init directives for the TARGET system, written into
# the installed system and processed on its first boot.
runcmd:
- echo "first boot on the installed system"
cloud-init runs in the ephemeral installer system and again on the target system's first boot, then, per the same page, "becomes inert for every subsequent reboot". That is why the nested user-data is the place for first boot work such as joining a configuration management system, and why it is the wrong place for anything that must run every boot.
There is one timing difference worth knowing early. Users created in identity exist when the installer finishes. Users created in user-data are only created on first boot. If a late-commands step needs the user's home directory, create the user with identity.
What an autoinstall.yaml looks like, section by section
Below is a complete server config with every section this article discusses. It is long on purpose; trim it down to what you need. Every key is documented in the autoinstall configuration reference.
#cloud-config
autoinstall:
version: 1
# Stop on no screens. Add names (for example "network") to keep them.
interactive-sections: []
# Runs before disks and network are probed.
early-commands:
- echo "autoinstall started" > /dev/console
locale: en_US.UTF-8
keyboard:
layout: us
timezone: Etc/UTC
# Do not silently switch installer versions mid fleet.
refresh-installer:
update: false
source:
id: ubuntu-server-minimal
search_drivers: false
network:
version: 2
ethernets:
primary:
match:
name: "en*"
dhcp4: true
apt:
preserve_sources_list: false
mirror-selection:
primary:
- uri: "http://mirror.internal.example/ubuntu"
fallback: abort
geoip: false
storage:
layout:
name: lvm
sizing-policy: all
match:
ssd: true
size: largest
password: "TEMPORARY-ROTATE-ON-FIRST-BOOT"
identity:
hostname: build-host-01
username: opsadmin
realname: "Ops Admin"
# Generate with: mkpasswd --method=SHA-512 --rounds=500000
password: "$6$rounds=500000$REPLACE_WITH_SALT$REPLACE_WITH_HASH"
ssh:
install-server: true
authorized-keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIREPLACE opsadmin@workstation"
allow-pw: false
kernel:
flavor: generic
packages:
- chrony
- auditd
# Exact version strings come from: apt-cache policy jq
- jq=1.7.1-3build1
snaps: []
updates: security
kernel-crash-dumps:
enabled: false
debconf-selections: |
ufw ufw/enable boolean true
late-commands:
- |
mkdir -p /target/var/lib/install-manifest
dpkg-query --admindir=/target/var/lib/dpkg -W \
-f='${Package}\t${Version}\t${Architecture}\n' \
> /target/var/lib/install-manifest/packages.tsv
- curtin in-target -- apt-mark hold jq
error-commands:
- tar -czf /installer-logs.tar.gz /var/log/installer/
- journalctl -b > /installer-journal.log
reporting:
builtin:
type: print
user-data:
disable_root: true
ssh_pwauth: false
runcmd:
- [systemctl, enable, --now, chrony]
shutdown: reboot
Now each part in turn.
version and interactive-sections
version must be 1. interactive-sections is a list of screens the installer should still show, using the values in your file as defaults. A single * turns the whole file into a set of defaults for a normal interactive install. One side effect: if any section is interactive, the reporting key is ignored, because reporting only applies to fully automated runs.
Since Ubuntu 24.04, when a file on the install media uses a top level autoinstall: key, any other key placed next to it is a validation error at run time. Unknown keys inside autoinstall: only produce warnings in version 1, and the reference says later versions will make them fatal. Treat every warning as an error in your own testing so a typo never silently drops a setting.
early-commands
early-commands run as soon as the installer starts, before block and network devices are probed. The installer re-reads /autoinstall.yaml after they run, so an early command can replace the config. The reference shows exactly that pattern: fetch a fresh config from a trusted server with wget -O /autoinstall.yaml. It also shows a pause loop that waits for a flag file, which is handy when you want to SSH into the live environment and look around before anything touches a disk.
Every command list in autoinstall follows the same rules: a string runs through sh -c, a list runs directly, everything runs as root, and any non-zero exit aborts the installation (except in error-commands).
locale, keyboard and timezone
The keyboard keys map onto /etc/default/keyboard. One known issue in the reference matters for encryption: with TPM-backed encryption and a passphrase, the disk unlocks only with the primary keyboard layout.
refresh-installer
When update is true, Subiquity refreshes itself from a snap channel (default stable/ubuntu-$REL) before continuing. That pulls bug fixes, but it also means two machines installed from the same ISO a month apart may run different installer code. For a fleet, leave it off and refresh your ISO deliberately, or turn it on in a test VM first and record which installer version you validated.
source
source.id chooses which install source on the ISO to use. On Ubuntu Server, ubuntu-server is the default and ubuntu-server-minimal is the smaller option. On Ubuntu Desktop the default is ubuntu-desktop-minimal and the full set is ubuntu-desktop. The reference says the canonical list lives on each ISO in casper/install-sources.yaml, and IDs can change, so check the file on the ISO you actually ship. search_drivers: false disables the third party drivers search and screen.
network
The network key takes Netplan format and applies both during installation and on the installed system. With no network key, the installer runs DHCPv4 on any interface named eth* or en* and disables interfaces that get no address. Matching by name pattern, as in the example, is safer than hard coding enp0s31f6 when your hardware is not identical.
proxy and apt
proxy is used during installation and written into apt and snapd config on the target. The reference notes it is not honoured for the geoip lookup.
The apt section uses curtin's APT format with Subiquity extensions. Inside mirror-selection, primary is a list of candidate mirrors; the installer tests them and uses the first that works. fallback decides what happens when none work: abort, offline-install or continue-anyway. Note the subtle default. The documented default configuration sets fallback: abort, but the field's own default is listed as offline-install. For fleet builds, set it explicitly. abort is usually right, because a silent offline install skips security updates and produces a machine that differs from its siblings.
geoip: false stops the installer from choosing a country mirror, but the reference is clear that it does not stop the request to geoip.ubuntu.com, which is also used for time zone detection. If your build network must not reach the internet, block it at the network layer rather than relying on this key.
storage
Storage is where unattended installs do damage, so read this section twice.
With no storage key on a single disk system, the installer uses the lvm layout. On a multi disk system there is no default and the install fails. Layouts install to the largest disk unless you give a match spec. The match keys include model, vendor, path, id_path, devpath, serial, ssd and size (largest or smallest), with globbing on most. A special install-media: true selects the disk the installer booted from, and the reference warns you to be careful not to overwrite the installer itself.
The supported layouts are:
| Layout | What you get | Encryption option |
|---|---|---|
lvm (default) | GPT, boot partition, /boot, LVM volume group with root logical volume | password: adds LUKS under LVM |
direct | GPT, plain partitions, no LVM | None in the layout |
zfs | bpool and rpool ZFS pools | Hybrid LUKS keystore plus ZFS native encryption, per the security overview |
hybrid with encrypted: yes | TPM-backed full disk encryption | TPM sealed key; accepted-errors for known pre-install issues |
The lvm layout's sizing-policy controls how much of the volume group the root volume gets. The default scaled policy uses all space under 10 GiB, 10 GiB for 10 to 20 GiB, half for 20 to 200 GiB, and 100 GiB above 200 GiB. Many people are surprised by an install that leaves most of a 1 TB disk unallocated; sizing-policy: all gives root the lot.
For anything the layouts cannot express, the config key accepts a curtin style action list (disks, partitions, LVM, RAID, formats, mounts) with Subiquity's extensions: sizes like 20G or 50%, and -1 to fill the remaining space on the last partition. Since Subiquity 24.08.1, a match can be an ordered list, so you can try a serial number first and fall back to a path.
identity
identity creates the first user during installation. It is the only required section unless you supply user-data, in which case it becomes optional. The password must be a crypt hash, not plain text. The reference says the password is required for sudo even when SSH key access is configured, and suggests mkpasswd (from the whois package) or openssl passwd to make one; cloud-init's docs recommend SHA-512 with 500,000 rounds, as in Step 2 below. Quote the hash in YAML; it is full of $ signs.
groups either replaces the default supplementary groups (a list, or override:) or adds to them (append:). The installer always creates a personal group named after the user; that cannot be turned off.
active-directory and ubuntu-pro
active-directory joins a domain during install. The admin account password is requested at run time, which means a domain join is not fully unattended. ubuntu-pro.token attaches a subscription. Treat that token as a secret (see the secrets section below).
ssh
install-server installs OpenSSH on the target. Desktop ISOs do not include openssh-server, so on Desktop this needs archive access. authorized-keys installs public keys for the first user. allow-pw defaults to true if there are no keys and false if there are. The reference labels install-server: true, a key list and allow-pw: false as its recommended configuration.
codecs, drivers and oem
codecs.install adds ubuntu-restricted-addons. drivers.install installs third party drivers suggested by ubuntu-drivers. oem.install defaults to auto, which installs OEM meta-packages on Desktop but not Server. The reference warns that OEM packages can pull a specific kernel, so combining oem.install: true with a pinned kernel can fail.
packages, snaps and kernel
packages is a list passed straight to apt-get install, so tasks (dns-server^) and exact versions (my-package=1-1) both work. snaps takes a name plus optional channel and classic. kernel takes either a package (an exact package name) or a flavor (generic, hwe); the reference says Server generally gets generic and Desktop gets hwe by default.
debconf-selections
This is where preseed knowledge still pays off. Lines in debconf-selections are fed to debconf in the target. The reference's own example carries a warning worth repeating: the OpenSSH debconf question permit-root-login is phrased the opposite way to the PermitRootLogin setting, so true actually means prohibit-password. Read package postinst scripts before trusting a debconf key's name.
updates, kernel-crash-dumps and shutdown
updates defaults to security, which installs from the -security pocket before the first reboot. all also pulls -updates. The Subiquity security overview confirms security updates are always applied when the installer can reach the archive. kernel-crash-dumps.enabled is null by default, which since 24.10 means dynamic enablement on qualifying amd64, arm64 and s390x systems. shutdown is reboot or poweroff; poweroff is useful for golden image builds where you capture the disk afterwards.
late-commands and error-commands
late-commands run after a successful installation and package installs, just before reboot. They run in the installer environment with the new system mounted at /target. To run something inside the installed system, prefix it with curtin in-target followed by a double dash separator, the same idea as in-target in a d-i preseed.
error-commands run after a failure, with whatever was installed still mounted at /target and logs in /var/log/installer. The reference example tars the installer logs and saves the journal. On a fleet, send those somewhere you can read them; otherwise a failed install on a machine in a rack is just a blinking cursor.
reporting
reporting sends progress events to destinations: print (the default, to tty1 and serial consoles), webhook (JSON POSTs to a URL using curtin's format), rsyslog (the reference notes this reporter "does not yet exist") and none. A webhook is the simplest way to watch fifty installs from one terminal. The webhook config can take OAuth style consumer and token secrets, which again belong outside a file you pass around.
user-data
user-data is a full cloud-init config for the installed system, merged with what the installer generates, and processed on first boot. cloud-init's module reference documents the keys: users (with lock_passwd, hashed_passwd, ssh_authorized_keys, sudo), ssh_pwauth, disable_root, write_files, runcmd and many more. Note that cloud-init 24.2 deprecated the hyphenated spellings such as hashed-passwd in favour of underscores, and that passwd only applies when the user does not exist yet, while hashed_passwd applies even to an existing user.
How to deliver the config
The Subiquity docs describe two families of delivery: cloud-init user data containing an autoinstall: key, or a file directly on the install media. The cloud-init route is the one the docs call "the suggested way", because you do not have to modify the install media.
| Route | Where the file lives | File format | Good for | Watch out for |
|---|---|---|---|---|
| NoCloud seed volume | user-data and meta-data at the root of a vfat or iso9660 volume labelled CIDATA | #cloud-config with autoinstall: | USB installs, VMs, unmodified ISOs | Label must be exactly CIDATA; meta-data must exist |
| NoCloud over the network | user-data and meta-data on an HTTP, HTTPS or FTP(S) server | #cloud-config with autoinstall: | PXE and fleet installs | URL needs a trailing slash; escape ; in GRUB; prefer HTTPS |
| On the install media | autoinstall.yaml at the root of the ISO filesystem (next to casper/) | Bare keys, or a single autoinstall: key (24.04 and later) | A USB stick that always installs the same thing | Not a cloud-config file; no other top level keys allowed |
| Kernel command line path | subiquity.autoinstallpath=path/to/autoinstall.yaml relative to the installer root | Same as on media | Custom images | Usually requires modifying the ISO |
| Desktop side load (24.04 and later) | A URL typed into the Desktop installer | Autoinstall YAML | One off Desktop installs | Interactive by design |
Order of precedence
If several sources exist, Subiquity uses the first it finds in this order: the kernel command line, the root of the installation system, cloud-config, then the root of the installation medium. A stray autoinstall.yaml on a USB stick therefore loses to a cloud-config seed, which surprises people who have both.
The confirmation prompt
The installer has a safety check meant to stop a USB stick with an autoinstall file from wiping the wrong machine. Before it changes the target disk, it prints a prompt asking Continue with autoinstall? (yes|no). Adding the word autoinstall to the kernel command line skips that prompt. For PXE installs into a known build VLAN, add it. For USB sticks handed to people, leave it off, so someone has to type "yes" at the machine.
The NoCloud seed in detail
cloud-init's NoCloud datasource reads four files from a seed: user-data, meta-data, vendor-data and network-config. The documentation says meta-data is required to contain an instance-id, but the autoinstall quick start creates it as an empty file with touch meta-data, and that works for installer seeds. Put an instance-id in it anyway when you also use the seed for cloud images; it is how cloud-init decides whether an instance is new.
NoCloud is told where to look by a "line configuration" on the kernel command line or in the DMI (SMBIOS) system serial number, for example ds=nocloud;s=https://10.42.42.42/configs/. The s (seedfrom) value must end in a slash. The docs warn that an unescaped semicolon on a GRUB linux line ends the statement, so escape it with a backslash in grub.cfg. The DMI route is useful in virtual machines and on some bare metal tooling, because you can set it without touching the boot loader. cloud-init can also expand DMI variables such as __dmi.system-serial-number__ inside the seed URL, which lets one HTTP server hand each machine its own config.
Network installs (PXE)
The Ubuntu Server netboot guide describes the flow: the machine PXE boots, DHCP points it at a boot loader on TFTP, the boot loader fetches a kernel, initrd and command line, and the initrd downloads the live server ISO from the url= you give it and loop mounts it. From there the install proceeds as if booted from local media. The guide's example uses dnsmasq for DHCP and TFTP, a signed grubx64.efi for UEFI so Secure Boot accepts it, and a kernel line that includes root=/dev/ram0 ramdisk_size=1500000 ip=dhcp url=...iso.
To make that unattended, add autoinstall and a NoCloud line configuration to the same kernel line:
linux /vmlinuz root=/dev/ram0 ramdisk_size=1500000 ip=dhcp \
url=http://mirror.internal.example/iso/ubuntu-26.04-live-server-amd64.iso \
autoinstall ds=nocloud\;s=https://deploy.internal.example/seeds/build-host/
initrd /initrd
The guide's own example sets cloud-config-url=/dev/null; drop that line or leave it, but test that your seed is still picked up. Host the ISO on your own mirror rather than pulling it from the public daily image path the example uses; a daily image is not a release and will change under you.
How Ubuntu Desktop autoinstall and the cloud-init datasource differ
People mix up three different things that all involve YAML and the word "init".
| Question | Ubuntu Server autoinstall | Ubuntu Desktop autoinstall | cloud-init on a cloud image |
|---|---|---|---|
| What reads the config? | Subiquity (server installer) | The Desktop installer, backed by Subiquity | cloud-init on the running instance |
| When does it run? | During installation, from a live ISO | During installation, from a live ISO | Every first boot of a new instance |
| Supported since | Server 20.04 | Desktop 23.04 (24.04 is the first LTS) | Long before both |
| Default source | ubuntu-server | ubuntu-desktop-minimal | Not applicable; the image is the source |
| Default kernel | Generally generic | Generally hwe | Whatever the image ships |
| SSH server | On the ISO | Not on the ISO; needs archive access | Image dependent |
identity required? | Yes, unless user-data is present | Optional, per the validator docs | Not used; users instead |
OEM meta-packages (oem: auto) | Not installed | Installed | Not applicable |
| Validator assumptions | Matches the validator | Validator assumes Server and may flag valid Desktop files | cloud-init schema |
You will often see 24.04 quoted as the start of Desktop autoinstall. The Subiquity docs say 23.04, which matches the 23.04 release notes, so this guide uses 23.04. In practice most fleets standardise on an LTS, which makes 24.04 the first realistic Desktop target.
The cloud-init datasource on a cloud image is a different job. On a cloud VM or a NoCloud seeded cloud image, there is no installer: the disk image is already Ubuntu, and cloud-init applies user-data on first boot. The autoinstall user-data key reuses exactly that mechanism on bare metal, which is why everything you know about cloud-init users, write_files and runcmd carries over.
How to set up full disk encryption, SSH keys and no password logins safely
Full disk encryption
LUKS on LVM. Add password: to an lvm layout. The Subiquity security overview describes the result: a boot loader partition, an ext4 /boot partition, and a LUKS device used as the LVM physical volume, with root in a logical volume. It also says plainly that "/boot and any data on the bootloader partition remain unencrypted". That is normal for this design, but it means kernel and initrd integrity depends on Secure Boot, not on LUKS.
The catch for unattended installs is that someone has to type the passphrase at every boot, and that the passphrase is written in plain text in your config. The safe pattern is to install with a temporary passphrase and change it during first boot or handover:
Warning: you can lock yourself out. Changing LUKS key slots on the wrong device, or removing the only working passphrase, makes the data unrecoverable. Check the device path first, add the new key before removing the old one, and test the new key before you reboot.
# 1. Identify the LUKS partition. Confirm the device path and size match
# the disk you installed to before running anything else.
lsblk -f
sudo cryptsetup luksDump /dev/nvme0n1p3 | head -20
# 2. Add a new passphrase (you will be asked for the temporary one first).
sudo cryptsetup luksAddKey /dev/nvme0n1p3
# 3. Prove the new passphrase works without rebooting.
sudo cryptsetup open --test-passphrase /dev/nvme0n1p3 && echo "new key OK"
# 4. Only then remove the temporary passphrase (enter the temporary one).
sudo cryptsetup luksRemoveKey /dev/nvme0n1p3
TPM-backed encryption. The hybrid layout with encrypted: yes seals the key to the TPM so there is no boot time passphrase. The 24.04 release notes describe it as introduced in 23.10 as an experimental Desktop feature that requires TPM 2.0, and list known issues, including that it is incompatible with some firmware anti-theft agents and needs a specific kernel snap that may lack modules such as vmd for NVMe RAID. In autoinstall, accepted-errors lists pre-install checks you accept so the install proceeds. The reference lists values such as running-in-vm, insufficient-dma-protection and weak-secure-boot-algorithms-detected, and warns that listing one "means you accept the associated risk". Use running-in-vm in your test VM. Do not copy it into the production file.
ZFS. The zfs layout creates bpool and rpool. With encryption, the security overview describes a LUKS "keystore" device inside rpool that holds the real ZFS key as a file. The 24.04 notes say guided ZFS is "without encryption by default".
SSH keys and no password logins
The goal on a server is usually: key based SSH only, no root login, and a user that can still use sudo. There are two workable patterns.
Pattern A: keys for SSH, password for sudo. Use identity with a strong, unique password hash and ssh.allow-pw: false. SSH accepts only keys; sudo still asks for the password. This is what the reference recommends, and it means a stolen SSH key alone does not give root.
autoinstall:
identity:
hostname: web-01
username: opsadmin
password: "$6$rounds=500000$REPLACE_WITH_SALT$REPLACE_WITH_HASH"
ssh:
install-server: true
authorized-keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIREPLACE opsadmin@workstation"
allow-pw: false
user-data:
disable_root: true
ssh_pwauth: false
Pattern B: no local password at all. Skip identity and create the user in user-data with lock_passwd: true, keys in ssh_authorized_keys, and a sudo rule. With a locked password, sudo cannot ask for one, so the sudo rule has to be NOPASSWD. That is common on cloud images, but understand the trade: whoever holds the private key is root. Pair it with hardware backed SSH keys or short lived SSH certificates, and keep console access for recovery.
autoinstall:
ssh:
install-server: true
allow-pw: false
user-data:
disable_root: true
ssh_pwauth: false
users:
- name: opsadmin
shell: /bin/bash
groups: [adm, sudo]
lock_passwd: true
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIREPLACE opsadmin@workstation"
Remember the timing: Pattern B's user only exists after first boot completes. If first boot fails before users runs, you have no login at all, so keep console or out of band access for the first rollout.
One more thing about the installer itself. The security overview says the Server ISO runs an SSH server during installation with a 20 character random password generated per boot and shown on the console, and that the installer's default user can become root with sudo at any time. Install on networks where that is acceptable, and treat the build VLAN as privileged.
How to test in a VM before rolling out
Every change to an autoinstall file should run in a VM first. The official quick start uses QEMU/KVM. The steps below follow it, with verification and validation added.
Step 1: download and verify the ISO
Ubuntu's verification tutorial checks a signed checksum file, then the ISO against it.
mkdir -p ~/iso && cd ~/iso
# Download the ISO, SHA256SUMS and SHA256SUMS.gpg from releases.ubuntu.com
gpg --keyid-format long --keyserver hkp://keyserver.ubuntu.com \
--recv-keys 0x46181433FBB75451 0xD94AA3F0EFE21092
gpg --keyid-format long --verify SHA256SUMS.gpg SHA256SUMS
sha256sum -c SHA256SUMS 2>&1 | grep OK
The tutorial lists the expected fingerprints, including 8439 38DF 228D 22F7 B374 2BC0 D94A A3F0 EFE2 1092 for the 2012 CD image signing key. A "Good signature" with a trust warning is expected unless you have certified the key yourself. Our post on writing and verifying an ISO covers this in more depth.
Step 2: write and validate the config
Write user-data and meta-data in a working directory, then validate with both tools.
mkdir -p ~/seed && cd ~/seed
cat > user-data <<'EOF'
#cloud-config
autoinstall:
version: 1
identity:
hostname: test-vm
username: opsadmin
password: "$6$rounds=500000$REPLACE_WITH_SALT$REPLACE_WITH_HASH"
ssh:
install-server: true
authorized-keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIREPLACE opsadmin@workstation"
allow-pw: false
storage:
layout:
name: lvm
late-commands:
- |
mkdir -p /target/var/lib/install-manifest
dpkg-query --admindir=/target/var/lib/dpkg -W \
-f='${Package}\t${Version}\t${Architecture}\n' \
> /target/var/lib/install-manifest/packages.tsv
EOF
echo "instance-id: test-vm-001" > meta-data
# Generate the password hash (whois package provides mkpasswd)
mkpasswd --method=SHA-512 --rounds=500000
# Subiquity's validator (run as a normal user, never with sudo)
git clone https://github.com/canonical/subiquity.git ~/src/subiquity
cd ~/src/subiquity && make install_deps
./scripts/validate-autoinstall-user-data.py ~/seed/user-data
# Add -vvv for the same detail the installer would print
# cloud-init's schema check for the cloud-config wrapper
cloud-init schema -c ~/seed/user-data --annotate
The Subiquity validator returns exit code 0 on success and 1 on failure, so it drops straight into CI. Its documented limits matter: it assumes an Ubuntu Server target with a synthesized install source, so a valid Desktop file without identity, or a source.id other than the synthesized one, can fail validation while being fine on the real ISO. It also cannot check things only knowable at install time, such as whether a match spec finds a disk. If your file is meant for the install media rather than cloud-config, pass the validator's no-expect-cloudconfig flag.
Step 3: build a seed ISO and a blank disk
sudo apt install cloud-image-utils qemu-system-x86 ovmf
cloud-localds ~/seed.iso ~/seed/user-data ~/seed/meta-data
truncate -s 20G ~/disk.img
cloud-localds produces a small ISO labelled CIDATA, which the NoCloud datasource finds on its own.
Step 4: install with the seed attached
# UEFI firmware from the ovmf package; file names vary by release,
# so check them with: dpkg -L ovmf | grep -i code
cp /usr/share/OVMF/OVMF_VARS_4M.fd $HOME/vars.fd
kvm -no-reboot -m 4096 -smp 2 \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,file=$HOME/vars.fd \
-drive file=$HOME/disk.img,format=raw,cache=none,if=virtio \
-drive file=$HOME/seed.iso,format=raw,cache=none,if=virtio \
-cdrom $HOME/iso/ubuntu-26.04-live-server-amd64.iso
The quick start's version boots in legacy BIOS mode; adding OVMF gives you UEFI, which is what most real hardware uses, and is needed if you test the TPM-backed layout with a software TPM. Without autoinstall on the kernel command line, the installer stops at the confirmation prompt; type yes, or interrupt the boot loader and add autoinstall to the linux line. -no-reboot makes QEMU exit when the installer reboots, so the next step starts from a clean state.
For the network route, the quick start serves user-data with python3 -m http.server 3003 and boots the ISO's own kernel and initrd directly with an append line containing autoinstall and a NoCloud URL pointing at _gateway:3003. That is quick for iteration because you edit the file and boot again without rebuilding a seed ISO.
Step 5: boot the result and inspect it
kvm -no-reboot -m 4096 -smp 2 \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,file=$HOME/vars.fd \
-drive file=$HOME/disk.img,format=raw,cache=none,if=virtio
Then, inside the VM:
cloud-init status --long # did first boot finish, with or without warnings?
sudo cloud-init schema --system --annotate
lsblk -f # layout and encryption as expected?
sudo sshd -T | grep -Ei 'passwordauthentication|permitrootlogin'
apt-mark showhold # pins in place?
cat /var/lib/install-manifest/packages.tsv | wc -l
sudo grep -rl "TEMPORARY-ROTATE-ON-FIRST-BOOT" /var/log /var/lib/cloud 2>/dev/null
The Ubuntu 24.04 release notes say cloud-init status now exits 2 on recoverable warnings and 1 on errors, which makes it a good CI gate. The last command is a habit worth building: search the installed system for distinctive strings from your config, such as a temporary passphrase or token, and see where copies landed. Do it on your release and installer version rather than trusting any list, including this one.
Step 6: automate the test
Put steps 2 to 5 in a pipeline: validate, build the seed, install headless with a serial console, boot, run the checks over SSH, and fail on any non-zero result. A nightly run against the current point release ISO catches installer changes before they reach hardware.
Rolling out to hardware with a CIDATA USB stick
On hardware you have two USB options: write the ISO to one stick and put the seed on a second stick labelled CIDATA, or add autoinstall.yaml to the ISO stick's data partition. The first keeps the ISO pristine and lets you swap configs by swapping a small stick.
Warning: this erases the target USB device.
mkfsanddddestroy everything on the device you name. Confirm the device path every time; device letters change between boots and between machines.
# 1. Identify the stick: check size, model and that nothing is mounted from it.
lsblk -o NAME,SIZE,MODEL,TRAN,MOUNTPOINTS
# 2. Set the path you have just confirmed, and check it once more.
DEV=/dev/sdX
lsblk "$DEV"
# 3. Create one FAT partition labelled CIDATA and copy the seed files.
sudo parted --script "$DEV" mklabel gpt mkpart primary fat32 1MiB 100%
sudo mkfs.vfat -n CIDATA "${DEV}1"
sudo mount "${DEV}1" /mnt
sudo cp ~/seed/user-data ~/seed/meta-data /mnt/
sudo umount /mnt
The NoCloud docs require the volume label to be CIDATA and the files to be at the root of the filesystem. If you prefer the single stick route, the Subiquity docs say to copy autoinstall.yaml to the partition containing the ISO contents, the directory that holds casper/. That file is not cloud-config: either bare keys or a single top level autoinstall: key, and nothing else at the top level.
If the target disk has old LVM or multipath metadata, the 24.04 release notes describe a known failure and suggest running wipefs on the disk from the installer shell or from early-commands. That is a destructive command, so if you automate it, guard it: match the disk by serial in a script, refuse to run if more than one disk matches, and never point it at install-media.
How to avoid putting secrets in the config file
An autoinstall file tends to accumulate secrets: a password hash, a LUKS passphrase, an Ubuntu Pro token, webhook OAuth secrets, a domain join account name, and whatever late-commands need to enrol the machine somewhere. Several things make this riskier than it looks.
- The LUKS passphrase is plain text. The
storage.layout.passwordfield takes the passphrase itself. - Hashes can be cracked. Many rounds of SHA-512 slow an attacker down, but a weak password inside the hash is still weak.
- Copies spread. The installer keeps the active config at
/autoinstall.yamlin the live environment, cloud-init keeps its artifacts, including the seed directory, under/var/lib/cloudon the installed system, and the installer writes logs of its own. Check where your release puts copies rather than assuming. cloud-init's instance data docs say it separates sensitive data so that only root can read it and returns redacted values to other users; that helps, but root on the installed system can still see what was passed in. - Transport is often plain HTTP. The quick start serves configs from
python3 -m http.server, which is fine in a VM on your laptop and wrong on a shared network. NoCloud supports HTTPS seeds; use them. - cloud-init itself warns in its
hashed_passwddocs that a password "might possibly be visible to unprivileged users" depending on whether the metadata service is reachable from unprivileged processes.
Practical patterns that work:
- Separate secret from structure. Keep the YAML in Git with placeholders and render the real file at deploy time from your secrets manager. Never commit the rendered file.
- Use temporary credentials and rotate on first boot. Install with a temporary LUKS passphrase and a throwaway password, then rotate both during first boot or handover, as shown above.
- Fetch secrets late, over an authenticated channel. In
user-dataruncmd, call an enrolment endpoint that authenticates the machine (for example with a one time token that expires in minutes, or a TPM-bound identity if your tooling supports it) and returns the long lived credentials. The one time token in the file is useless after first use. - Use the early-commands handoff. The reference's own example replaces
/autoinstall.yamlwith one fetched from a trusted server. Combined with a per machine URL built from the DMI serial, the seed on the USB stick can be generic and secret free. - Serve per machine configs from short lived HTTPS URLs, log every fetch, and delete the rendered file once the webhook reports success.
- Prefer keys to passwords. Public SSH keys are not secrets, so a key-only config has much less to protect.
- Search the installed system for distinctive strings from your config after every installer upgrade, as in Step 5.
How to pin package versions and capture an SBOM of the installed image
"Same config, same machine" only holds if everything the config pulls is fixed. An autoinstall run pulls from at least five places that can drift: the installer itself (refresh-installer), the archive mirror, the security pocket (updates), snaps, and anything late-commands downloads.
What to pin, and how
| Drift source | Pin it with | Notes |
|---|---|---|
| Installer code | refresh-installer.update: false and a fixed ISO | Record the ISO file name and SHA-256 |
| Base packages on the ISO | The ISO's .manifest file | releases.ubuntu.com publishes a .manifest next to each image |
| Extra packages | packages: [name=version] plus apt-mark hold in late-commands | Exact versions must exist in the mirror you use |
| Kernel | kernel.package: linux-image-... | Do not combine with oem.install: true |
| Mirror contents | An internal mirror you snapshot, or the Ubuntu snapshot service | fallback: abort so a dead mirror does not mean a different machine |
| Security updates at install | updates: security (default) or a snapshot | A trade between patched and reproducible |
| Snaps | channel: per snap, and a snap proxy or revision control | Channels move; record revisions installed |
Downloads in late-commands | Fixed URLs plus checksum checks | Never pipe a URL into a shell |
The Ubuntu snapshot service, documented in the Server guide, serves the archive as it was at any time after 1 March 2023, addressed by a UTC timestamp such as 20240430T214500Z. On Ubuntu 24.04 and later, apt detects snapshot support automatically; 22.04 needs apt 2.4.11 and 20.04 needs apt 2.0.10, plus a snapshot=yes option in the sources entries. You can pass a snapshot per command or set it for the whole system with an APT::Snapshot setting in /etc/apt/apt.conf.d/. The same docs show snapshot packages being served from snapshot.ubuntu.com, under a path that ends in the timestamp.
There are two ways to use it with autoinstall, and neither is a documented autoinstall recipe, so test both in your VM pipeline:
autoinstall:
# Option 1: point the install mirror at a fixed snapshot
apt:
mirror-selection:
primary:
- uri: "https://snapshot.ubuntu.com/ubuntu/20261015T000000Z"
fallback: abort
geoip: false
# Option 2: pin the installed system's apt to the snapshot after install
late-commands:
- >-
echo 'APT::Snapshot "20261015T000000Z";'
> /target/etc/apt/apt.conf.d/50snapshot
After the test install, run apt policy on a few packages and check that every pocket, including security, resolves the way you expect. Remember to move the snapshot forward on a schedule. A frozen snapshot is reproducible and also stops receiving security fixes, which is the opposite of what you want on a running server.
Capturing the package manifest
The late-commands step in the examples above writes a tab separated list of every installed Debian package to /var/lib/install-manifest/packages.tsv on the target, using dpkg-query against the target's dpkg database from the installer environment. Snaps are not visible to dpkg; add a first boot step to record them:
user-data:
runcmd:
- [sh, -c, "snap list --all > /var/lib/install-manifest/snaps.txt"]
Copy both files off the machine, through your webhook reporting or a first boot upload, and store them with the ISO checksum and the config's Git commit. That triple (ISO hash, config commit, manifest) is what lets you answer "what exactly is on that server" a year later.
Turning the manifest into an SBOM
A package list becomes far more useful as a Software Bill of Materials in a standard format. CycloneDX (an Ecma standard, ECMA-424, currently version 1.7) and SPDX (ISO/IEC 5962:2021) are the two common choices. Debian and Ubuntu packages have a Package URL type, deb, whose documented examples look like pkg:deb/ubuntu/dpkg@1.19.0.4?arch=amd64, with characters such as + percent encoded in versions.
The script below converts the manifest into a minimal CycloneDX JSON document. It uses only the Python standard library.
#!/usr/bin/env python3
"""Convert a dpkg-query manifest (name<TAB>version<TAB>arch) to CycloneDX JSON."""
import datetime
import json
import sys
import uuid
from urllib.parse import quote
manifest, distro, hostname, out = sys.argv[1:5]
components = []
with open(manifest, encoding="utf-8") as fh:
for line in fh:
parts = line.rstrip("\n").split("\t")
if len(parts) != 3:
continue
name, version, arch = parts
purl = (f"pkg:deb/ubuntu/{name}@{quote(version, safe=':~')}"
f"?arch={arch}&distro={distro}")
components.append({
"type": "library",
"bom-ref": purl,
"name": name,
"version": version,
"purl": purl,
})
bom = {
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": f"urn:uuid:{uuid.uuid4()}",
"version": 1,
"metadata": {
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"component": {
"type": "operating-system",
"name": hostname,
"bom-ref": f"host:{hostname}",
},
},
"components": components,
}
with open(out, "w", encoding="utf-8") as fh:
json.dump(bom, fh, indent=2)
print(f"wrote {len(components)} components to {out}")
python3 manifest-to-cdx.py packages.tsv noble build-host-01 build-host-01.cdx.json
Set specVersion to what your downstream tools accept; 1.6 is widely supported and 1.7 is current. This SBOM records what dpkg says is installed. It does not see software copied in by late-commands, Python or Node packages installed with their own package managers, snaps, or firmware. For those, run an ecosystem aware SBOM tool over the installed filesystem or the relevant project directories, and merge the results.
Common mistakes
- Missing
#cloud-configon the first line. cloud-init ignores the file and you get a fully interactive install. The validator catches this. - Autoinstall keys at the top level of cloud-config. They must sit under
autoinstall:. Top level keys configure only the live installer. - A cloud-config file used as
autoinstall.yamlon the media. On the media, onlyautoinstall:(or bare keys) may appear at the top level. - Expecting
user-datausers to exist inlate-commands. They are created on first boot. Useidentityif late commands need the user. - Multi disk machines without a storage match. There is no default for multiple disks, and on a single disk the layout picks the largest. Match by serial or model, or by
size: smallestfor a boot SSD. - Leaving the default
scaledsizing policy and wondering why a large disk has 100 GiB of root and the rest unallocated. - An unescaped semicolon in the GRUB
ds=nocloud;s=line. GRUB ends the statement and cloud-init never sees the URL. - No trailing slash on the seedfrom URL. NoCloud requires it.
- Copying
accepted-errors: [running-in-vm]from the test file into production. In production, review every pre-install error you accept. refresh-installer.update: trueon a fleet without recording which installer version ran.- Pinning
kernel.packageand enablingoem.install. The reference warns the two can conflict. - Downloading and running scripts in
late-commandswithout checksums. That is an unpinned, unsigned dependency in the most privileged step of the install. - Only checking the happy path. Add
error-commandsthat save logs where you can read them.
FAQ
Does Ubuntu still support preseed?
Not on Ubuntu Server's current installer. Since 20.04, the live server installer (Subiquity) uses autoinstall YAML, and since 23.04 the Desktop installer does too. The debconf-selections key still lets you pass debconf answers to packages in the target, which covers many old preseed lines, but the installer questions themselves must be rewritten as autoinstall keys.
What is the difference between autoinstall and cloud-init?
Autoinstall configures the installation: disks, users, packages, mirrors. cloud-init is the delivery mechanism for that config and then, through the nested user-data key, configures the installed system on its first boot. cloud-init does not process autoinstall directives itself; Subiquity does.
Where do I put autoinstall.yaml on a USB stick?
At the root of the partition that holds the ISO contents, next to the casper directory. Alternatively, use a second USB volume labelled CIDATA containing user-data (with #cloud-config and an autoinstall: key) and a meta-data file. The docs recommend the cloud-init route because it leaves the install media unchanged.
How do I stop the "Continue with autoinstall?" prompt?
Add the word autoinstall to the kernel command line, either in your PXE boot loader config or by editing the boot entry. Keep the prompt for USB sticks that might be booted on the wrong machine; it is there to stop an accidental wipe.
Can I autoinstall Ubuntu Desktop?
Yes. The Subiquity docs list Desktop 23.04 and later. The Desktop defaults differ: the minimal desktop source, the hwe kernel flavour, OEM meta-packages installed when available, and no SSH server on the ISO. Ubuntu 24.04 also lets you side load an autoinstall.yaml from a URL inside the Desktop installer.
How do I generate the password hash for autoinstall?
Use mkpasswd from the whois package with the SHA-512 method and 500,000 rounds (the exact command is in Step 2), or openssl passwd. Put the result in quotes in the YAML. The identity password must be a hash, never plain text, and it is still required for sudo when you use SSH keys.
How do I test an autoinstall config without a spare machine?
Use QEMU/KVM as in the official quick start: build a CIDATA seed with cloud-localds, create a blank disk image, and boot the ISO with both attached. Validate the file first with Subiquity's validate-autoinstall-user-data.py and cloud-init schema.
Where are the logs when an autoinstall fails?
In the live installer environment under /var/log/installer, plus the system journal. Add error-commands that archive them, and a reporting webhook, so failures on remote hardware are visible. On the installed system, the long form of cloud-init status and /var/log/cloud-init.log cover first boot.
Can autoinstall encrypt the disk without a passphrase at boot?
Yes, with the TPM-backed hybrid layout and encrypted: yes, on hardware with TPM 2.0. It began as an experimental feature in 23.10 and has known hardware limits listed in the 24.04 release notes, so test on your exact models and plan recovery before rolling it out.
How do I make every install identical?
Fix the ISO (and record its hash), turn off refresh-installer, point apt at a mirror you control or a dated snapshot, pin package and kernel versions, set snap channels, verify any downloads in late-commands, and capture a package manifest during install so you can prove what landed.
Where Safeguard fits, and where it does not
Safeguard does not install operating systems, manage devices, protect endpoints or inspect firmware, and nothing in this guide depends on it. Where it can help is the software inventory that an unattended install produces. The manifest and CycloneDX file from the previous section are ordinary SBOM files, and the Safeguard CLI can scan an SBOM file and match its components against vulnerability data, with results written as JSON:
safeguard scan --sbom build-host-01.cdx.json
The same CLI scans the repositories that hold your autoinstall templates and any helper scripts or applications that late-commands deploy, and the platform shows which of your projects contain a given component and version. Ubuntu's own security notices and the distribution's updates remain the source of fixes for packages from the Ubuntu archive.
A machine built from a config is only as current as the components the config pulls in, and every rebuild pulls them again: packages from the mirror, snaps, and whatever application dependencies the first boot steps install. The steps below apply to the open source dependencies and container images you build and ship on top of the OS, which is where Safeguard's remediation and Gold fit, while the base packages still come from Ubuntu.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos install Ubuntu or touch an autoinstall file. They show the software side of a machine you have just built from one.
This is a real Safeguard CLI session: install, open the interactive terminal, ask it to fix a lodash CVE, and approve each command as it edits package.json to lodash 4.17.21 and runs npm install and npm test, with nothing pushed. On a freshly autoinstalled build host, the same flow is how you would check and fix the application repositories you clone back onto it, approving every command yourself.
Click through it at your own pace, or open it full screen.
This demo sets up the Safeguard runner on your own machine, approves it with a device code, and runs a scan from the terminal. If you keep autoinstall templates, late-commands scripts and the SBOMs from your installs in a repository, the same runner can scan that repository on hardware you control.
Click through it at your own pace, or open it full screen.
Here Claude connects to the Safeguard MCP server with OAuth, and you choose which Safeguard tools Claude may use. That is useful when you want to ask in plain language which of your projects carry a given component version after a rebuild, while keeping the assistant limited to the tools you picked.
Click through it at your own pace, or open it full screen.
To try it on your own projects, download Safeguard or read the CLI documentation.
Quick check
You put an identity section and a runcmd list at the top level of a cloud-config seed, next to autoinstall. What happens?
The installer reads nothing from them for the target system. Top level cloud-config keys only configure the temporary live installer environment, so runcmd runs in the live session and disappears at reboot, and identity is not a cloud-config key at all. Autoinstall keys belong under autoinstall:, and first boot cloud-init keys belong under autoinstall: user-data:.
Why is installing with a temporary LUKS passphrase and rotating it on first boot safer than putting the real passphrase in the config?
The storage.layout.password value is plain text, and the config is copied to several places during installation and first boot. A temporary passphrase that is replaced (new key added and tested, old key removed) soon after install means any copy of the file that leaks no longer unlocks the disk.
Sources
- Introduction to autoinstall, Ubuntu installation documentation
- Autoinstall configuration reference manual
- Autoinstall JSON schema
- Autoinstall quick start
- Providing autoinstall configuration
- Cloud-init and autoinstall interaction
- Autoinstall validation
- Zero-touch deployment with autoinstall
- Subiquity security overview
- NoCloud datasource, cloud-init documentation
- User-data formats, cloud-init documentation
- Instance-data, cloud-init documentation
- Module reference, cloud-init documentation
- CLI commands, cloud-init documentation
- How to debug user-data, cloud-init documentation
- How to netboot the server installer on amd64, Ubuntu Server documentation
- How to use the Ubuntu snapshot service, Ubuntu Server documentation
- How to verify your Ubuntu download, Ubuntu tutorials
- Ubuntu 20.04 LTS release notes
- Ubuntu 23.04 release notes
- Ubuntu 24.04 LTS (Noble Numbat) release notes
- List of Ubuntu releases
- Ubuntu 24.04 release images and manifests, releases.ubuntu.com
- Ubuntu snapshot archive, snapshot.ubuntu.com
- curtin documentation
- CycloneDX specification overview
- SPDX specifications
- Package URL deb type definition, purl-spec
Last reviewed: 24 October 2026. Installer and cloud-init options change between releases; check the autoinstall reference and cloud-init documentation for the version on your ISO.