Safeguard
Supply Chain Security•

Ubuntu Autoinstall and cloud-init: Unattended Installs Step by Step

Autoinstall replaced preseed on Ubuntu Server 20.04. Write a YAML config, deliver it via cloud-init NoCloud, test in a VM, keep secrets out, pin versions.

Safeguard Security Team
49 min read

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 under autoinstall: user-data: are cloud-init directives that run on the installed system's first boot. Plain #cloud-config keys at the top level only touch the temporary live installer environment.
  • Three delivery routes. A cloud-init NoCloud seed (a volume labelled CIDATA or an HTTP URL on the kernel command line) is the route the docs recommend. An autoinstall.yaml at the root of the install media also works. The kernel command line subiquity.autoinstallpath= parameter wins over everything else.
  • The installer asks before wiping a disk unless autoinstall is 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 ships cloud-init schema. Both catch the common mistakes: a missing #cloud-config header, a misspelled autoinstall: key, a string where a list belongs.
  • Reproducible means pinned. Use package=version in the packages list, a fixed kernel package, a fixed snap channel, an internal or snapshot mirror, and capture a package manifest during late-commands so 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

DateRelease or componentWhat changed for unattended installsSource
23 Apr 2020Ubuntu 20.04 LTSLive server installer becomes the preferred Server media; automated installs supportedUbuntu 20.04 release notes; Subiquity intro
20 Apr 2023Ubuntu 23.04Desktop installer becomes a Subiquity backed app; autoinstall supported on Desktop from this releaseUbuntu 23.04 release notes; Subiquity intro
12 Oct 2023Ubuntu 23.10TPM-backed full disk encryption introduced as an experimental Desktop featureUbuntu 24.04 release notes
25 Apr 2024Ubuntu 24.04 LTSA 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-configSubiquity docs; Ubuntu 24.04 release notes
2024Subiquity 24.08.1Disk match specs can be an ordered list (try serial first, then path)Autoinstall reference
10 Oct 2024Ubuntu 24.10kernel-crash-dumps defaults to dynamic enablement when hardware qualifiesAutoinstall reference
23 Apr 2026Ubuntu 26.04 LTSCurrent LTS used in the official quick startUbuntu list of releases; autoinstall quick start

Preseed compared with autoinstall

Behaviourdebian-installer preseedSubiquity autoinstall
File formatdebconf-set-selections linesYAML, validated against a JSON schema
Unanswered questionInstaller stops and asksInstaller takes the default (fails if there is no default)
Mixed modePartial preseed, then promptsinteractive-sections lists screens to still show
StoragePartman recipesNamed layouts (lvm, direct, zfs, hybrid) or a curtin style action list
Post install hookspreseed/late_command with in-targetlate-commands with curtin in-target
First boot configSeparate toolingNested cloud-init user-data
Error handlingVarieserror-commands run, traceback printed, installer waits
Debconf answersNativeStill 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:

LayoutWhat you getEncryption option
lvm (default)GPT, boot partition, /boot, LVM volume group with root logical volumepassword: adds LUKS under LVM
directGPT, plain partitions, no LVMNone in the layout
zfsbpool and rpool ZFS poolsHybrid LUKS keystore plus ZFS native encryption, per the security overview
hybrid with encrypted: yesTPM-backed full disk encryptionTPM 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.

RouteWhere the file livesFile formatGood forWatch out for
NoCloud seed volumeuser-data and meta-data at the root of a vfat or iso9660 volume labelled CIDATA#cloud-config with autoinstall:USB installs, VMs, unmodified ISOsLabel must be exactly CIDATA; meta-data must exist
NoCloud over the networkuser-data and meta-data on an HTTP, HTTPS or FTP(S) server#cloud-config with autoinstall:PXE and fleet installsURL needs a trailing slash; escape ; in GRUB; prefer HTTPS
On the install mediaautoinstall.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 thingNot a cloud-config file; no other top level keys allowed
Kernel command line pathsubiquity.autoinstallpath=path/to/autoinstall.yaml relative to the installer rootSame as on mediaCustom imagesUsually requires modifying the ISO
Desktop side load (24.04 and later)A URL typed into the Desktop installerAutoinstall YAMLOne off Desktop installsInteractive 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".

QuestionUbuntu Server autoinstallUbuntu Desktop autoinstallcloud-init on a cloud image
What reads the config?Subiquity (server installer)The Desktop installer, backed by Subiquitycloud-init on the running instance
When does it run?During installation, from a live ISODuring installation, from a live ISOEvery first boot of a new instance
Supported sinceServer 20.04Desktop 23.04 (24.04 is the first LTS)Long before both
Default sourceubuntu-serverubuntu-desktop-minimalNot applicable; the image is the source
Default kernelGenerally genericGenerally hweWhatever the image ships
SSH serverOn the ISONot on the ISO; needs archive accessImage dependent
identity required?Yes, unless user-data is presentOptional, per the validator docsNot used; users instead
OEM meta-packages (oem: auto)Not installedInstalledNot applicable
Validator assumptionsMatches the validatorValidator assumes Server and may flag valid Desktop filescloud-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. mkfs and dd destroy 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.password field 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.yaml in the live environment, cloud-init keeps its artifacts, including the seed directory, under /var/lib/cloud on 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_passwd docs 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:

  1. 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.
  2. 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.
  3. Fetch secrets late, over an authenticated channel. In user-data runcmd, 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.
  4. Use the early-commands handoff. The reference's own example replaces /autoinstall.yaml with 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.
  5. Serve per machine configs from short lived HTTPS URLs, log every fetch, and delete the rendered file once the webhook reports success.
  6. Prefer keys to passwords. Public SSH keys are not secrets, so a key-only config has much less to protect.
  7. 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 sourcePin it withNotes
Installer coderefresh-installer.update: false and a fixed ISORecord the ISO file name and SHA-256
Base packages on the ISOThe ISO's .manifest filereleases.ubuntu.com publishes a .manifest next to each image
Extra packagespackages: [name=version] plus apt-mark hold in late-commandsExact versions must exist in the mirror you use
Kernelkernel.package: linux-image-...Do not combine with oem.install: true
Mirror contentsAn internal mirror you snapshot, or the Ubuntu snapshot servicefallback: abort so a dead mirror does not mean a different machine
Security updates at installupdates: security (default) or a snapshotA trade between patched and reproducible
Snapschannel: per snap, and a snap proxy or revision controlChannels move; record revisions installed
Downloads in late-commandsFixed URLs plus checksum checksNever 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-config on 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.yaml on the media. On the media, only autoinstall: (or bare keys) may appear at the top level.
  • Expecting user-data users to exist in late-commands. They are created on first boot. Use identity if 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: smallest for a boot SSD.
  • Leaving the default scaled sizing 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: true on a fleet without recording which installer version ran.
  • Pinning kernel.package and enabling oem.install. The reference warns the two can conflict.
  • Downloading and running scripts in late-commands without checksums. That is an unpinned, unsigned dependency in the most privileged step of the install.
  • Only checking the happy path. Add error-commands that 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.