diff --git a/ansible/apt.yml b/ansible/apt.yml
new file mode 100644
index 0000000000000000000000000000000000000000..907e7ec62c83740d4f5d9c3b1eff560f9f13fe39
--- /dev/null
+++ b/ansible/apt.yml
@@ -0,0 +1,12 @@
+---
+- name: Ensure APT is configured
+  hosts: seed-hypervisor:seed:overcloud
+  vars:
+    ansible_python_interpreter: /usr/bin/python3
+  tags:
+    - apt
+  tasks:
+    - name: include apt role
+      include_role:
+        name: apt
+      when: ansible_facts.os_family == 'Debian'
diff --git a/ansible/group_vars/all/apt b/ansible/group_vars/all/apt
index 93e47260427ff3a59b452cbb65eb3e9ab5cc5c53..fad722dcd15cf98ec0605f5c6b64354234bf6c98 100644
--- a/ansible/group_vars/all/apt
+++ b/ansible/group_vars/all/apt
@@ -4,3 +4,9 @@
 
 # Apt cache TTL in seconds. Default is 3600.
 apt_cache_valid_time: 3600
+
+# Apt proxy URL for HTTP. Default is empty (no proxy).
+apt_proxy_http:
+
+# Apt proxy URL for HTTPS. Default is {{ apt_proxy_http }}.
+apt_proxy_https: "{{ apt_proxy_http }}"
diff --git a/ansible/roles/apt/defaults/main.yml b/ansible/roles/apt/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fad722dcd15cf98ec0605f5c6b64354234bf6c98
--- /dev/null
+++ b/ansible/roles/apt/defaults/main.yml
@@ -0,0 +1,12 @@
+---
+###############################################################################
+# Apt package manager configuration.
+
+# Apt cache TTL in seconds. Default is 3600.
+apt_cache_valid_time: 3600
+
+# Apt proxy URL for HTTP. Default is empty (no proxy).
+apt_proxy_http:
+
+# Apt proxy URL for HTTPS. Default is {{ apt_proxy_http }}.
+apt_proxy_https: "{{ apt_proxy_http }}"
diff --git a/ansible/roles/apt/tasks/main.yml b/ansible/roles/apt/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..16205b6be9e2176cd85f4b4ce36bbe709534db91
--- /dev/null
+++ b/ansible/roles/apt/tasks/main.yml
@@ -0,0 +1,17 @@
+---
+- name: Configure apt proxy
+  template:
+    src: "01proxy.j2"
+    dest: /etc/apt/apt.conf.d/01proxy
+    owner: root
+    group: root
+    mode: 0664
+  become: true
+  when: apt_proxy_http | default('', true) | length > 0 or apt_proxy_https | default('', true) | length > 0
+
+- name: Remove old apt proxy config
+  file:
+    path: /etc/apt/apt.conf.d/01proxy
+    state: absent
+  become: true
+  when: apt_proxy_http | default('', true) | length == 0 and apt_proxy_https | default('', true) | length == 0
diff --git a/ansible/roles/apt/templates/01proxy.j2 b/ansible/roles/apt/templates/01proxy.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b76a9e3b036cdf5a5cbdfb0cd4576cb13a62c8bf
--- /dev/null
+++ b/ansible/roles/apt/templates/01proxy.j2
@@ -0,0 +1,8 @@
+Acquire {
+{% if apt_proxy_http | default('', true) | length > 0 -%}
+  HTTP::proxy "{{ apt_proxy_http }}";
+{% endif -%}
+{% if apt_proxy_https | default('', true) | length > 0 -%}
+  HTTPS::proxy "{{ apt_proxy_https }}";
+{% endif -%}
+}
diff --git a/doc/source/configuration/reference/hosts.rst b/doc/source/configuration/reference/hosts.rst
index 86c3c2652f9bfa37cdda5bb9a8bf0473299c381e..c8f99d6a769e263cf126ec638ee04c7a068f144c 100644
--- a/doc/source/configuration/reference/hosts.rst
+++ b/doc/source/configuration/reference/hosts.rst
@@ -307,6 +307,10 @@ Apt cache
 The Apt cache timeout may be configured via ``apt_cache_valid_time`` (in
 seconds) in ``etc/kayobe/apt.yml``, and defaults to 3600.
 
+Apt can be configured to use a proxy via ``apt_proxy_http`` and
+``apt_proxy_https`` in ``etc/kayobe/apt.yml``. These should be set to the full
+URL of the relevant proxy (e.g. ``http://squid.example.com:3128``).
+
 SELinux
 =======
 *tags:*
diff --git a/etc/kayobe/apt.yml b/etc/kayobe/apt.yml
index 552a116cf88a3f3e75ee2d42a48fc8d1afe1c0c2..5f278e322222111933c9d60d487edf780a291238 100644
--- a/etc/kayobe/apt.yml
+++ b/etc/kayobe/apt.yml
@@ -5,6 +5,12 @@
 # Apt cache TTL in seconds. Default is 3600.
 #apt_cache_valid_time:
 
+# Apt proxy URL for HTTP. Default is empty (no proxy).
+#apt_proxy_http:
+
+# Apt proxy URL for HTTPS. Default is {{ apt_proxy_http }}.
+#apt_proxy_https:
+
 ###############################################################################
 # Dummy variable to allow Ansible to accept this file.
 workaround_ansible_issue_8743: yes
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 9fd8414926b69c4b1eca89208a6035869610f894..8a7ffc6fdadb4a3620cbfa0ff3eca8289869c1cc 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -451,7 +451,7 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin,
 
         playbooks = _build_playbook_list(
             "ssh-known-host", "kayobe-ansible-user",
-            "dnf", "pip", "kayobe-target-venv")
+            "apt", "dnf", "pip", "kayobe-target-venv")
         if parsed_args.wipe_disks:
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
@@ -605,7 +605,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         # Run kayobe playbooks.
         playbooks = _build_playbook_list(
             "ssh-known-host", "kayobe-ansible-user",
-            "dnf", "pip", "kayobe-target-venv")
+            "apt", "dnf", "pip", "kayobe-target-venv")
         if parsed_args.wipe_disks:
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
@@ -994,7 +994,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         # Kayobe playbooks.
         playbooks = _build_playbook_list(
             "ssh-known-host", "kayobe-ansible-user",
-            "dnf", "pip", "kayobe-target-venv")
+            "apt", "dnf", "pip", "kayobe-target-venv")
         if parsed_args.wipe_disks:
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index c94f34b3305e80a9ed8a9bd7297d3fc17713b155..80e9145280eee6a0dbe240d04d2e45a10e5c2336 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -328,6 +328,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ssh-known-host.yml"),
                     utils.get_data_files_path(
                         "ansible", "kayobe-ansible-user.yml"),
+                    utils.get_data_files_path("ansible", "apt.yml"),
                     utils.get_data_files_path("ansible", "dnf.yml"),
                     utils.get_data_files_path("ansible", "pip.yml"),
                     utils.get_data_files_path(
@@ -498,6 +499,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ssh-known-host.yml"),
                     utils.get_data_files_path(
                         "ansible", "kayobe-ansible-user.yml"),
+                    utils.get_data_files_path("ansible", "apt.yml"),
                     utils.get_data_files_path("ansible", "dnf.yml"),
                     utils.get_data_files_path("ansible", "pip.yml"),
                     utils.get_data_files_path(
@@ -1080,6 +1082,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ssh-known-host.yml"),
                     utils.get_data_files_path(
                         "ansible", "kayobe-ansible-user.yml"),
+                    utils.get_data_files_path("ansible", "apt.yml"),
                     utils.get_data_files_path("ansible", "dnf.yml"),
                     utils.get_data_files_path("ansible", "pip.yml"),
                     utils.get_data_files_path(
diff --git a/releasenotes/notes/add-apt-proxy-support-f688702868095ed0.yaml b/releasenotes/notes/add-apt-proxy-support-f688702868095ed0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a6bbba40558399b54da401d6a7d99396bc2af7ff
--- /dev/null
+++ b/releasenotes/notes/add-apt-proxy-support-f688702868095ed0.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds support for configuring apt's proxy setting for Ubuntu hosts.
+    See `story 2009035
+    <https://storyboard.openstack.org/#!/story/2009035>`_ for details.