cloud-init Logs and Instance Data: Linux Cloud VM Forensics
cloud-init on Linux cloud VMs: cloud-init.log, output log, user-data, per-instance state and boot scripts that record provisioning, SSH keys and persistence.
- Location
- /var/log/cloud-init.log, /var/log/cloud-init-output.log, /var/lib/cloud/
- Proves
- How and when a cloud VM was provisioned, which user-data and SSH keys it received, which instance IDs the disk has booted as, and what boot scripts run
- Timestamps
- Log lines 'YYYY-MM-DD hh:mm:ss,mmm' (UTC in current releases); semaphore and state file times; boot-finished content
- Access
- root for user-data and sensitive instance data; logs usually root-readable only or adm group
- Retention
- Logs rotated by size where the distro ships a logrotate snippet; /var/lib/cloud persists for the life of the disk; /run/cloud-init is lost at reboot
- Collection
- UAC, Velociraptor, cp -a, cloud-init collect-logs
Tools
Compare all tools- cloud-initCLI · open source
- jqCLI · open source
- grep / zgrepCLI · built into Linux
- journalctlCLI · built into Linux
What it is
cloud-init is the first-boot provisioning agent on almost every official Linux cloud image: Ubuntu, Debian cloud images, RHEL, Rocky, Alma, Fedora Cloud and SUSE images on AWS, Azure, GCP, OpenStack and most other clouds. It reads metadata and user-data from the platform, then sets the hostname, creates users, installs SSH keys, writes files, installs packages and runs scripts. Ubuntu Server also uses it on bare-metal installs.
For an investigation it answers questions that are otherwise hard on a cloud VM: which image and instance this disk came from, what the operator (or an attacker with cloud API access) passed as user-data, and whether anything is set to run on every boot.
Where it lives
| Item | Path | Notes |
|---|---|---|
| Main log | /var/log/cloud-init.log | Debug log of every stage and module |
| Output log | /var/log/cloud-init-output.log | stdout and stderr of runcmd, scripts and package installs |
| Current instance | /var/lib/cloud/instance | Symlink to instances/<instance-id>/ |
| Instance state | /var/lib/cloud/instances/<instance-id>/ | user-data.txt, user-data.txt.i, vendor-data.txt, cloud-config.txt, scripts/, sem/, boot-finished, obj.pkl |
| Host-wide data | /var/lib/cloud/data/ | instance-id, previous-instance-id, result.json, status.json, set-hostname |
| Recurring scripts | /var/lib/cloud/scripts/{per-boot,per-instance,per-once}/ | Executed by the scripts_per_* modules |
| Configuration | /etc/cloud/cloud.cfg, /etc/cloud/cloud.cfg.d/*.cfg | Datasource, default user, modules |
| Runtime data | /run/cloud-init/instance-data.json (redacted), instance-data-sensitive.json (root only) | Rewritten every boot, gone after shutdown |
What it proves
- Provisioning time and each boot's stages, with module results (
cc_ssh,cc_users_groups,cc_runcmd...). - The exact user-data supplied to the instance, including scripts,
runcmdcommands, created users andssh_authorized_keys. Changing user-data through the cloud API and rebooting is a known way to get code execution on a VM. - Which instance IDs this disk has run as: one directory per ID under
instances/, plusprevious-instance-id. A disk restored from a snapshot or cloned into an attacker's account shows a new ID. - What cloud-init will run on the next boot (
per-bootscripts,bootcmdentries in config), which is persistence as root. - It does not record commands run later over SSH; it only covers what cloud-init itself executed.
Key fields
2026-09-20 03:14:07,512 - util.py[DEBUG]: Cloud-init v. 24.4 running 'init' at Sun, 20 Sep 2026 03:14:07 +0000. Up 6.31 seconds.
2026-09-20 03:14:09,118 - cc_ssh.py[DEBUG]: Writing authorized keys for user ubuntu
2026-09-20 03:14:12,904 - subp.py[DEBUG]: Running command ['/var/lib/cloud/instance/scripts/runcmd'] with allowed return codes [0]
| Item | Meaning |
|---|---|
Cloud-init v. X running 'init'/'modules:config'/'modules:final' | Start of a stage; also shows uptime, which helps order boots |
sem/config_<module> | Semaphore files; their mtime is when that per-instance module last ran |
boot-finished | Uptime, time and version of the last completed boot |
data/result.json | Datasource used and errors of the last run |
user-data.txt | Raw user-data (may be MIME multipart or gzip); .i is the processed form |
scripts/part-00N, scripts/runcmd | Scripts extracted from user-data and the generated runcmd script |
Timestamps
Current cloud-init releases force the log formatter to UTC. Older releases wrote local time, which on cloud images is usually UTC anyway; confirm with /etc/localtime and the +0000 offset printed in the "running 'init'" line. Semaphore files and boot-finished give file system times that match stage completion.
Retention
Upstream ships a Debian-packaging logrotate snippet that rotates /var/log/cloud-init*.log at 1 MB, keeping 6 compressed generations; other distributions may not rotate these logs at all, so years of boots can sit in one file. The /var/lib/cloud/instances/ directories are never cleaned automatically. cloud-init clean --logs removes state and logs, and attackers or image-building scripts use it, so a missing /var/lib/cloud on an image that clearly booted in the cloud is notable.
Collection
# Dead box
tar -C /mnt/evidence -cpf /cases/2026-017/cloud-init.tar var/log/cloud-init.log* \
var/log/cloud-init-output.log* var/lib/cloud etc/cloud 2>/dev/null
# Live (root): includes /run data and a redacted bundle
cloud-init collect-logs --tarfile /media/ir/cloud-init.tar.gz
cp -a /run/cloud-init /media/ir/run-cloud-init
Whether cloud-init collect-logs includes user-data and sensitive instance data depends on the release, the flags (older versions need --include-userdata) and running as root, so also copy the raw files. UAC's var_log and etc artifacts pick up the logs and configuration, and dedicated artifacts cover some cloud agents (for example the AWS SSM and Azure VM agents); add /var/lib/cloud explicitly if your profile misses it.
Parsing
C=/cases/2026-017
ls -la --time-style=full-iso $C/var/lib/cloud/instances/ # every instance ID the disk ran as
cat $C/var/lib/cloud/data/instance-id $C/var/lib/cloud/data/previous-instance-id
grep -E "running '(init|init-local|modules:config|modules:final)'" $C/var/log/cloud-init.log*
grep -nE 'ssh_authorized_keys|runcmd|write_files|users:|bootcmd|curl|wget' \
$C/var/lib/cloud/instances/*/user-data.txt
ls -la --time-style=full-iso $C/var/lib/cloud/scripts/per-*/ $C/var/lib/cloud/instances/*/sem/
jq . $C/var/lib/cloud/data/result.json
On a live host, cloud-init query userdata, cloud-init status --long and cloud-init analyze show (per-stage timing) give the same data.
Investigator tips
- Compare user-data across instance directories. A second instance directory whose user-data adds an SSH key or a
runcmddownload points to someone with cloud control-plane access; confirm with the provider's audit log (CloudTrailModifyInstanceAttribute, Azure activity log). - Any file in
/var/lib/cloud/scripts/per-boot/runs as root on every boot. Treat it likerc.localand check it against boot hooks and systemd units. - User-data often contains secrets (tokens, database passwords, registration keys). Handle and report it accordingly, and assume an attacker with read access to it has them.
- The default user and keys created by cloud-init appear in passwd and authorized_keys; keys there that do not come from user-data or the platform key pair were added later.
cloud-init-output.logkeeps the console output of package installs and scripts, which sometimes includes the download URLs and hashes of tooling an attacker pushed through user-data.