← All notes

Open-sourcing cloud-image-inspector

cloud-image-inspector reads a cloud disk image without booting or mounting it and reports the OS, shells, privilege tool, init system and sshd facts that first-boot configuration depends on. Zero dependencies, Apache-2.0.

We are releasing cloud-image-inspector as open source under Apache-2.0. It reads a cloud disk image directly, without booting it or mounting it, and reports what is inside: the operating system, and the facts that decide how a first-boot configuration has to be written for that image. It is written in Rust and has no dependencies beyond the standard library.

The code is at github.com/virtainer/cloud-image-inspector. It is the second Virtainer component we have opened, after the Virtainer guest agent.

Why an image has to be read before it boots

Cloud images all accept cloud-init, so it is tempting to write one cloud-config and send it to every image. It does not work. A cloud-config names a login shell, grants a privilege tool, and touches the serial console and sshd, and each of those depends on what is actually in the image:

  • Login shell. /bin/bash is not on Alpine, so a user created with it cannot log in. On Debian, /bin/sh is dash, so a user given /bin/sh gets a shell that does not read the rc files they expect.
  • Privilege tool. Alpine ships doas, not sudo.
  • Serial console login. systemd images take a serial-getty@ttyS0 override; busybox-init images need a line in /etc/inittab, and whether that line uses util-linux agetty or busybox getty depends on which one the image has.
  • Key-only accounts. cloud-init locks an account that has no password. When sshd is built with PAM, a locked account still accepts its key. When it is not, as on Alpine, sshd refuses the key too, and the user sees Permission denied (publickey).

Guessing these from the distribution name is how Virtainer used to do it, and the guesses broke as soon as an image did not match them. Reading the image replaces the guess with the fact.

What it reports

$ cloud-image-inspector images/generic_alpine-3.24.1-x86_64-uefi-cloudinit-r0.qcow2
== images/generic_alpine-3.24.1-x86_64-uefi-cloudinit-r0.qcow2
container      qcow2 v3, virtual size 214.0 MiB, 64.0 KiB clusters, deflate compression
partitions     gpt table
   1  esp         vfat      512.0 KiB name="EFI"
   2  linux-data  ext4      212.0 MiB label="/" name="/"
boot           UEFI only
root           partition 2 (ext4)
os             Alpine Linux 3.24.1 (ID=alpine VERSION_ID=3.24.1)
shells         bash: no
               /bin/sh: /bin/sh -> /bin/busybox
privilege      sudo: no   doas: yes (/usr/bin/doas)
init           openrc (/sbin/init: /sbin/init -> /bin/busybox)
sshd           sshd: yes (/usr/sbin/sshd)   PAM build: yes (/usr/sbin/sshd.pam)
               UsePAM: (default: no)   sshd_config.d included: true
first boot     cloud-init: yes (/usr/bin/cloud-init), version 26.1-r3 (from apk database)
packages       apk (201 installed, /lib/apk/db/installed)

Besides the facts above, it reports the boot method (UEFI or BIOS), the root filesystem, including btrfs subvolumes and OSTree deployments, the first-boot agent (cloud-init and its version, tiny-cloud, or Ignition), and every installed package from the image’s own package database. --json emits all of it. ls, cat, stat and export subcommands read individual files out of the image the same way.

It reads qcow2 (v2 and v3, with deflate and zstd compressed clusters) and raw images; GPT and MBR partition tables; ext2/3/4, XFS and btrfs; and the apk, dpkg, pacman and RPM (SQLite and ndb) package databases. The README lists the exact coverage. LVM, LUKS, multi-device btrfs and RPM’s old Berkeley DB format are reported as unsupported rather than guessed at.

Images are untrusted input

An image comes from a URL or an upload, so the inspector treats every byte of it as hostile. It never executes guest code and never hands a guest filesystem to the host kernel to parse; every format is parsed in userspace by its own code.

  • Every on-disk read is bounds-checked, so malformed data becomes an error value, not a panic.
  • Every length that sizes an allocation is capped.
  • Every tree walk has both a depth cap and a node-visit budget, because corrupt pointers can form a graph that a depth cap alone does not stop.
  • qcow2 backing files and external data files are refused, since following them would open a host path the image chose.
  • Symlinks resolve inside the image root and never climb above it.

The zero-dependency rule is part of the same posture: there is no third-party parser in the path, and the project’s CI fails if one appears.

How it was checked

The parsers were verified against the kernel, not against themselves. On 13 real cloud images (Alpine, Debian, Ubuntu, Fedora, Fedora CoreOS, AlmaLinux, Rocky, RHEL 9 and 10, openSUSE and Arch), libguestfs boots a real Linux kernel that mounts the same root filesystem, and the whole tree it exports is compared with the inspector’s export entry by entry, down to the SHA-256 of every file. Package lists are compared with the distribution’s own package manager run against the same database. Synthetic images built with the real mkfs tools cover the edge cases that cloud images do not reach, and 13,500 corrupted images ran without a panic or a hang. VERIFICATION.md in the repository has the full record, and tools/verify-all.sh reruns it.

How Virtainer uses it

In Virtainer Free 1.1, every image that lands on a host, whether downloaded, fetched from a URL or uploaded, is read once by the inspector and its facts are stored next to it. Everything below is built from those facts:

  • An image that can only boot through BIOS is rejected when it is added, because classic VMs boot from UEFI firmware. Before, it was accepted and then booted to a black screen.
  • The login shell is the first of ash, bash, dash and zsh that the image actually has.
  • A key-only account is left unlocked only when the image’s sshd does not use PAM.
  • Console auto-login is written for the image’s init and getty, and turned off, with the reason shown, for an init system it has not been verified on.

License and contributions

The code is under the Apache License 2.0. Issues and pull requests are welcome. If the inspector misreads an image or fails on one, an issue naming the image and where it came from is the most useful report, because the verification suite can then be pointed at it. Please report vulnerabilities through Security → Report a vulnerability on the repository rather than a public issue.

KEEP READING
Introducing Virtainer Free 1.1Oct 6Open-sourcing the Virtainer guest agentOct 5