diff --git a/ansible/roles/infra-vms/tasks/deploy.yml b/ansible/roles/infra-vms/tasks/deploy.yml
index 71268061b1f2a7919d5fac7b920cb603c0ad25fe..e51991786afb73954869e5952f60badf403d94da 100644
--- a/ansible/roles/infra-vms/tasks/deploy.yml
+++ b/ansible/roles/infra-vms/tasks/deploy.yml
@@ -49,6 +49,26 @@
     mime: False
   register: stat_result
 
+# NOTE(mgoddard): Prior to the Xena release, the seed VM was provisioned using
+# the stackhpc.livirt-vm role with become=true. This resulted in the cached
+# image being owned by root. Since Xena, we execute the role without
+# become=true. Correct the image ownership to avoid a permission denied error
+# when downloading a new image of the same name.
+- name: "[{{ vm_name }}] Stat image files"
+  stat:
+    path: "{{ image_cache_path }}/{{ item.image | basename }}"
+  with_items: "{{ vm_hostvars.infra_vm_volumes | selectattr('image', 'defined') }}"
+  register: image_stat_result
+
+- name: "[{{ vm_name }}] Fix image ownership"
+  file:
+    path: "{{ image_cache_path }}/{{ item.item.image | basename }}"
+    owner: "{{ ansible_facts.user_uid }}"
+    group: "{{ ansible_facts.user_gid }}"
+  with_items: "{{ image_stat_result.results }}"
+  when: item.stat.exists
+  become: true
+
 - name: "[{{ vm_name }}] Ensure that the VM is provisioned"
   include_role:
     name: stackhpc.libvirt-vm
diff --git a/ansible/seed-vm-provision.yml b/ansible/seed-vm-provision.yml
index 2844798870b2670ef9f7ba1e54e928aeb78a183c..ea547fd714cccfb062a6ffc085fe14198c8262ff 100644
--- a/ansible/seed-vm-provision.yml
+++ b/ansible/seed-vm-provision.yml
@@ -29,6 +29,26 @@
         group: "{{ ansible_facts.user_gid }}"
       become: True
 
+    # NOTE(mgoddard): Prior to the Xena release, the seed VM was provisioned
+    # using the stackhpc.livirt-vm role with become=true. This resulted in the
+    # cached image being owned by root. Since Xena, we execute the role without
+    # become=true. Correct the image ownership to avoid a permission denied
+    # error when downloading a new image of the same name.
+    - name: Stat image files
+      stat:
+        path: "{{ image_cache_path }}/{{ item.image | basename }}"
+      with_items: "{{ hostvars[seed_host].seed_vm_volumes | selectattr('image', 'defined') }}"
+      register: image_stat_result
+
+    - name: Fix image ownership
+      file:
+        path: "{{ image_cache_path }}/{{ item.item.image | basename }}"
+        owner: "{{ ansible_facts.user_uid }}"
+        group: "{{ ansible_facts.user_gid }}"
+      with_items: "{{ image_stat_result.results }}"
+      when: item.stat.exists
+      become: true
+
   roles:
     - role: jriguera.configdrive
       # For now assume the VM OS family is the same as the hypervisor's.
@@ -99,7 +119,6 @@
           volumes: "{{ hostvars[seed_host].seed_vm_volumes + [seed_vm_configdrive_volume] }}"
           interfaces: "{{ hostvars[seed_host].seed_vm_interfaces }}"
           console_log_enabled: true
-      become: True
 
   tasks:
     - name: Wait for SSH access to the seed VM
diff --git a/releasenotes/notes/story-2009277-84c381a562244fab.yaml b/releasenotes/notes/story-2009277-84c381a562244fab.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f7d5d6ad5de46fb156d3823f906028b514518260
--- /dev/null
+++ b/releasenotes/notes/story-2009277-84c381a562244fab.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fixes an issue where cached seed VM images are unnecessarily owned by root.
+    See `story 2009277 <https://storyboard.openstack.org/#!/story/2009277>`__
+    for details.