NeuroDynamic.Tech
Sign inJoin

Snapshots, cloning, and the identity trap

Snapshots you can rewind to, and a clone that you break and then properly fix. The breaking is deliberate: cloning has a common identity trap.

The founder · 12 min read ·

DEEP END

Tested for real: I ran every command in this guide top-to-bottom on a fresh Ubuntu 24.04 virtual machine (4 CPUs, 8 GB RAM) on 13 August 2026. The run continued directly from the end state of Headless VMs. Versions at test time: Ubuntu 24.04.4 LTS, libvirt 10.0.0, QEMU 8.2.2, virt-clone 4.1.0, virt-sysprep 1.52.0.

Found a problem? Tell me via the contact page.

Placeholders: anything written in CAPS-WITH-DASHES, like YOUR-VM-IP-HERE or YOUR-VM2-IP-HERE, is a value you replace with your own — here that means the addresses from your lease table. $USER is different — leave it exactly as written; the shell fills in your username for you.

What you'll end up with

Snapshots you can rewind to, and a clone that you break and then properly fix. The breaking is deliberate: cloning has a common identity trap. I hit it for real in testing, and you will watch the whole diagnosis.

The finished state from my test run — two independent machines, born a few minutes apart from one image:

$ virsh net-dhcp-leases default
 Expiry Time           MAC address         Protocol   IP address           Hostname   Client ID or DUID
------------------------------------------------------------------------------------------------------
 2026-08-13 18:03:49   52:54:00:04:09:f8   ipv4       192.168.122.82/24    vm1        ff:56:...:20:99
 2026-08-13 18:15:37   52:54:00:42:32:0e   ipv4       192.168.122.153/24   vm2        ff:56:...:dd:d0

Who this is for

DEEP END. You have done Headless VMs: virt-install, cloud-init and snapshots without a desktop and kept its end state: vm1 running, reachable over SSH with the vm-lab-key. This guide starts exactly there.

Time and cost

The commands for this guide and the headless guide took me about 7 minutes of measured run time in total, as one continuous run. Budget 45 minutes for this half. Cost: nothing; every tool is in Ubuntu's archive.

One honesty note: my test machine is itself a virtual machine, so my guests are nested and their disk speed is the worst case. Your times may be better on real hardware, but storage and network speed still matter.

Words you'll meet

  • Internal snapshot — a saved moment (disk, and RAM if running) stored inside the qcow2 file. There are also external snapshots, discussed honestly below. (basics)
  • machine-id — a unique identity string every Linux install carries in /etc/machine-id. Clones inherit it, which causes real trouble. (basics)
  • DUID — the identifier a machine presents to DHCP. On Ubuntu it is derived from the machine-id — which is why cloned machine-ids poison DHCP. (basics)
  • DHCP lease — the network's record of "this machine has this IP". virsh net-dhcp-leases is how you find a headless guest. (basics)

Before you start

  • The end state of the headless guidevm1 running, the ~/.ssh/vm-lab-key pair, and the libvirt group membership.
  • Around 4 GB free disk and 4 GB free RAM while both guests run.

The steps

Step 1 — snapshots: save a moment, ruin things, rewind

An honest orientation first. Libvirt has two snapshot styles.

Internal snapshots live inside the qcow2 file: one file to manage, one command each way, and they can include RAM while the guest runs. The trade-offs: qcow2-only, slower on big disks, and some tooling (block commit, incremental backup workflows) does not support them.

External snapshots write changes to a new overlay file. Faster and more flexible — but you manage a chain of files, and merging them back is on you.

For learning and for small lab guests, internal is the honest, simple choice — that is what we test here. Know that external exists before you build production backups on either. This guide does not teach external snapshot cleanup or production backup design.

Take a snapshot of the running guest — RAM included:

virsh snapshot-create-as vm1 clean-state --description "Fresh vm1 before experiments"

The important line is:

Domain snapshot clean-state created

Two seconds, in my test — the guest never stopped running.

Now damage the future: create a file that the snapshot knows nothing about (from the host, in one line):

ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "echo do-not-lose-me > ~/precious.txt; ls -l ~/precious.txt"

Your output will vary, but look for:

-rw-rw-r-- 1 ubuntu ubuntu 15 Aug 13 17:02 /home/ubuntu/precious.txt

List snapshots:

virsh snapshot-list vm1

You should see something like:

 Name          Creation Time               State
----------------------------------------------------
 clean-state   2026-08-13 17:02:18 +0000   running

Rewind:

virsh snapshot-revert vm1 clean-state

A passing run includes:

Domain snapshot clean-state reverted

Three seconds, and the guest is running again immediately — reverting to a running snapshot restores the RAM too, no reboot.

Check it worked. The file made after the snapshot must be gone:

ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "ls -l ~/precious.txt"

You should see something like:

ls: cannot access '/home/ubuntu/precious.txt': No such file or directory

That error is the pass mark. Time travel, demonstrated. And the obvious warning made explicit: everything after a snapshot dies when you revert — snapshots are a lab tool and a pre-upgrade parachute, not a backup.

Step 2 — clone it, and watch the clone break

You want a second VM. Copying feels free — one command, no download. Do it, and then watch closely, because what happens next is the most valuable part of this guide.

Know the shape of the problem before you meet it. A clone is a full copy — including its identity. Making it a genuinely new machine means fixing four things: the machine-id, the netplan MAC pin, the hostname, and the SSH host keys. This step springs the trap; the next step fixes all four.

virt-clone needs the source shut off:

virsh shutdown vm1

Clone (8 seconds in my test):

virt-clone --original vm1 --name vm2 --auto-clone

The important line is:

Clone 'vm2' created successfully.

Two notes from my test before we boot anything. The snapshot did not come along — virsh snapshot-list vm2 is empty. And the seed CD was not copied — vm2 points at vm1's seed file (it is read-only, so virt-clone shares it). Both matter later.

Boot both:

virsh start vm1
virsh start vm2

Now the trap springs. vm1 comes back on its old address. vm2 boots, runs — and never appears:

virsh net-dhcp-leases default

You should see something like:

 Expiry Time           MAC address         Protocol   IP address          Hostname   Client ID or DUID
------------------------------------------------------------------------------------------------------
 2026-08-13 18:03:49   52:54:00:04:09:f8   ipv4       192.168.122.82/24   vm1        ff:56:...:20:99

One lease. I waited over four minutes in testing — vm2 never asked for an address at all.

Here is the diagnosis, exactly as I ran it. Read the clone's inherited network config through vm1 (they are identical clones, after all):

ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE "sudo cat /etc/netplan/50-cloud-init.yaml"

Your output will vary, but look for:

network:
  version: 2
  ethernets:
    enp1s0:
      match:
        macaddress: "52:54:00:04:09:f8"
      dhcp4: true

There it is. Cloud-init pinned the network card by MAC address on first boot. virt-clone — correctly — gave vm2 a fresh MAC:

virsh domiflist vm2

Your output will vary, but look for:

 Interface   Type      Source    Model    MAC
-------------------------------------------------------------
 vnet4       network   default   virtio   52:54:00:42:32:0e

Different MAC. The clone's netplan matches no interface, so it configures nothing — it never even asks for DHCP.

And lurking one layer down: the clone also inherited vm1's /etc/machine-id, from which Ubuntu derives its DHCP DUID. Even with netplan fixed, the two machines would present the same identity to the DHCP server. A clone is not a new machine until you make it one.

Step 3 — give the clone its own identity

Shut the clone down; we operate on its disk from outside:

virsh shutdown vm2

Install libguestfs — the toolkit for editing guest disks without booting them (110 packages, 42 seconds in my test):

sudo apt-get install -y libguestfs-tools

Eject the seed CD it shares with vm1 — the clone must not re-read vm1's first-boot config:

virsh change-media vm2 sda --eject --config

The important line is:

Successfully ejected media.

Reset the machine-id and set the hostname. One deliberate flag choice here: --enable machine-id,customize runs only those operations. virt-sysprep's default operation set also wipes authorized_keys — which would lock us out of our own clone:

sudo virt-sysprep -d vm2 --enable machine-id,customize --hostname vm2

A passing run includes:

[   0.0] Examining the guest ...
[   8.1] Performing "machine-id" ...
[   8.1] Performing "customize" ...
[   8.2] Setting the machine ID in /etc/machine-id
[   8.2] Setting the hostname: vm2

Un-pin the network card — rewrite the clone's netplan to plain "DHCP on enp1s0, any MAC":

sudo virt-customize -d vm2 --run-command "printf 'network:\n  version: 2\n  ethernets:\n    enp1s0:\n      dhcp4: true\n' > /etc/netplan/50-cloud-init.yaml && chmod 600 /etc/netplan/50-cloud-init.yaml"

A passing run includes:

[   0.0] Examining the guest ...
[  19.8] Running: printf [...snipped...]
[  19.9] Finishing off

Boot it:

virsh start vm2

Check it worked. 34 seconds later in my test:

virsh net-dhcp-leases default

You should see something like:

 Expiry Time           MAC address         Protocol   IP address           Hostname   Client ID or DUID
------------------------------------------------------------------------------------------------------
 2026-08-13 18:03:49   52:54:00:04:09:f8   ipv4       192.168.122.82/24    vm1        ff:56:...:20:99
 2026-08-13 18:15:37   52:54:00:42:32:0e   ipv4       192.168.122.153/24   vm2        ff:56:...:dd:d0

Two leases, two hostnames, two different DUIDs — that last column differing is the machine-id reset doing its job. Prove it end to end:

ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM2-IP-HERE "hostname; cat /etc/machine-id"

Your output will vary, but look for:

vm2
2336d8cbf5e0a8015956a0b344b0a4ea

vm1 reported b93b68cff1c749298b89ce14210e5df1 in my test — different identity, as required.

One last inherited secret: the clone still carries vm1's SSH host keys (I checksummed them — byte-identical). For a disposable lab this is acceptable; for anything real, regenerate them — otherwise two servers claim the same cryptographic identity. Regenerate inside vm2:

ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM2-IP-HERE "sudo rm /etc/ssh/ssh_host_*; sudo dpkg-reconfigure -f noninteractive openssh-server; sudo systemctl restart ssh; sudo md5sum /etc/ssh/ssh_host_ed25519_key.pub"

You should see something like:

4bfddc6cc86a97a47acccf89392dde2d  /etc/ssh/ssh_host_ed25519_key.pub

Different checksum from vm1's (mine was a36e8aa9...). Now they are genuinely two machines.

Something went wrong?

Everything below happened in my recorded run — most of it on purpose.

  • Your clone is running but never gets an IP → it means netplan is pinned to the original's MAC (and the machine-id is cloned underneath) → this is Steps 2-3's whole story; apply Step 3.
  • You see error: ... cannot delete inactive domain with 1 snapshots when undefining → it means snapshot metadata blocks the delete → add --snapshots-metadata (the headless guide's Undo shows it).

Undo all of this

Tested for real — as one combined undo with the headless guide. Here is the part that removes what this guide added; that guide's undo then clears vm1, the packages, and the folders.

Force the clone off and delete it:

virsh destroy vm2
virsh undefine vm2 --remove-all-storage

(No snapshots on vm2, so no extra flag needed.)

Two things this guide added live on vm1's side, and the headless guide's undo handles both. The clean-state snapshot goes when vm1 is undefined — that is why its undo carries --snapshots-metadata; I captured the refusal without it. And libguestfs-tools, installed here in Step 3, is included in that guide's package purge.

Check it worked.

virsh list --all

Only vm1 remains in the list. Then run the headless guide's Undo; I ran the two back-to-back as one sequence and ended with a completely clean machine.

Where to go next


Last tested: 13 August 2026 on Ubuntu 24.04.4 LTS. Versions: libvirt 10.0.0, QEMU 8.2.2, virt-clone 4.1.0, libguestfs/virt-sysprep 1.52.0. Tested as one continuous run with the headless VMs guide; transcripts live in ../guide-09-headless-vms/.


Tried it? Improved it?

Tell the forum what worked and what didn’t: real experience beats recommendations, and the best answers get folded back into this guide with credit.

Related guides