diff --git a/ansible/group_vars/all/kolla b/ansible/group_vars/all/kolla
index 33aef2c6959ffbccd32c0216c586b4529cd4e62e..932ce473819df185098ce58ee2d16f8efd7b200f 100644
--- a/ansible/group_vars/all/kolla
+++ b/ansible/group_vars/all/kolla
@@ -470,7 +470,7 @@ kolla_enable_barbican: "no"
 kolla_enable_blazar: "no"
 kolla_enable_ceilometer: "no"
 kolla_enable_central_logging: "no"
-kolla_enable_chrony: "yes"
+kolla_enable_chrony: "no"
 kolla_enable_cinder: "no"
 kolla_enable_cinder_backend_iscsi: "{{ kolla_enable_cinder_backend_lvm | bool or kolla_enable_cinder_backend_zfssa_iscsi | bool }}"
 kolla_enable_cinder_backend_lvm: "no"
diff --git a/ansible/group_vars/all/time b/ansible/group_vars/all/time
index a2b277c1eea3bc213afc1cc891f49fe368eaa4be..6ab7708e14ec1a0068ced3b213cbe9ca00df167c 100644
--- a/ansible/group_vars/all/time
+++ b/ansible/group_vars/all/time
@@ -6,3 +6,36 @@
 
 # Name of the local timezone.
 timezone: "{{ ansible_date_time.tz }}"
+
+###############################################################################
+# Network Time Protocol (NTP).
+
+# Kayobe default time sources
+chrony_ntp_servers_default:
+  - server: pool.ntp.org
+    type: pool
+    options:
+      - option: iburst
+      - option: minpoll
+        val: 8
+
+# List of NTP time sources to configure. Format is a list of dictionaries with
+# the following keys:
+# server:  host or pool
+# type:    (Optional) Defaults to server. Maps to a time source in the
+#           configuration file. Can be one of server, peer, pool.
+# options: (Optional) List of options that depends on type, see Chrony
+#          documentation for details.
+# See: https://chrony.tuxfamily.org/doc/4.0/chrony.conf.html
+#
+# Example of configuring a pool and customising the pool specific maxsources
+# option:
+# chrony_ntp_servers:
+#   - server: pool.ntp.org
+#     type: pool
+#     options:
+#       - option: maxsources
+#         val: 3
+#
+chrony_ntp_servers: "{{ chrony_ntp_servers_default }}"
+###############################################################################
diff --git a/ansible/roles/ntp/defaults/main.yml b/ansible/roles/ntp/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dea948074daae1ec66bc0bdb74af715a0f2a943c
--- /dev/null
+++ b/ansible/roles/ntp/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+ntp_actions: ["validate", "prepare", "deploy"]
+
+ntp_service_disable_list:
+  - ntp.service
+  - systemd-timesyncd.service
diff --git a/ansible/roles/ntp/tasks/deploy.yml b/ansible/roles/ntp/tasks/deploy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eee45417c9acec891bc950a65fc7f3007855de20
--- /dev/null
+++ b/ansible/roles/ntp/tasks/deploy.yml
@@ -0,0 +1,4 @@
+---
+- name: Import role to configure chrony
+  import_role:
+    name: mrlesmithjr.chrony
diff --git a/ansible/roles/ntp/tasks/main.yml b/ansible/roles/ntp/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..efb655192c667318f54946d79b9a344ad0c39663
--- /dev/null
+++ b/ansible/roles/ntp/tasks/main.yml
@@ -0,0 +1,12 @@
+---
+- name: Validate configuration
+  include_tasks: validate.yml
+  when: '"validate" in ntp_actions'
+
+- name: Pre-deploy preparation
+  include_tasks: prepare.yml
+  when: '"prepare" in ntp_actions'
+
+- name: Deploy service
+  include_tasks: deploy.yml
+  when: '"deploy" in ntp_actions'
diff --git a/ansible/roles/ntp/tasks/prepare.yml b/ansible/roles/ntp/tasks/prepare.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b9c2b70d03fa6c7a8a619032233132566bacaefa
--- /dev/null
+++ b/ansible/roles/ntp/tasks/prepare.yml
@@ -0,0 +1,27 @@
+---
+
+- name: Populate service facts.
+  service_facts:
+
+- name: Mask alternative NTP clients to prevent conflicts
+  vars:
+    service_exists: "{{ item in services }}"
+  systemd:
+    name: "{{ item }}"
+    enabled: "{{ 'false' if service_exists else omit }}"
+    masked: true
+    state: "{{ 'stopped' if service_exists else omit }}"
+  become: true
+  with_items: "{{ ntp_service_disable_list }}"
+
+- name: Remove kolla-ansible installed chrony container
+  docker_container:
+    name: chrony
+    state: absent
+  become: true
+  # NOTE(wszumski):  There is an ordering issue where on a fresh host, docker
+  # will not have been configured, but if that is the case, the chrony container
+  # can't possibly exist, but trying to execute this unconditionally will fail
+  # with: No module named 'docker' as we have not yet added the docker package
+  # to the kayobe target venv.
+  when: "'docker.service' in services"
diff --git a/ansible/roles/ntp/tasks/validate.yml b/ansible/roles/ntp/tasks/validate.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0d1b4266e23a94308d634628d269e0cc419e622c
--- /dev/null
+++ b/ansible/roles/ntp/tasks/validate.yml
@@ -0,0 +1,11 @@
+---
+
+- name: Validate structure of chrony_ntp_servers dictionary
+  assert:
+    that:
+      - chrony_ntp_servers is sequence
+      - chrony_ntp_servers | selectattr('server', 'undefined') | list | length == 0
+    msg: "chrony_ntp_servers set to invalid value"
+  when:
+    - chrony_ntp_servers is defined
+    - chrony_ntp_servers | length > 0
diff --git a/ansible/time.yml b/ansible/time.yml
new file mode 100644
index 0000000000000000000000000000000000000000..609723fa1caa8d922532e173a53c49f7417ee03f
--- /dev/null
+++ b/ansible/time.yml
@@ -0,0 +1,18 @@
+---
+- name: Ensure timezone is configured
+  hosts: seed-hypervisor:seed:overcloud
+  tags:
+    - timezone
+  tasks:
+    - import_role:
+        name: yatesr.timezone
+      become: True
+
+- name: Ensure Chrony is installed and configured
+  hosts: ntp
+  tags:
+    - ntp
+  tasks:
+    - import_role:
+        name: ntp
+      when: not kolla_enable_chrony | bool
diff --git a/ansible/timezone.yml b/ansible/timezone.yml
index 2bf278743ad34048aa0a26ca4167f42f5815d695..c8b718910d985ca8be6221aee6a4111b4b442a4b 100644
--- a/ansible/timezone.yml
+++ b/ansible/timezone.yml
@@ -1,8 +1,22 @@
 ---
-- name: Ensure timezone is configured
-  hosts: seed-hypervisor:seed:overcloud
-  tags:
-    - timezone
-  roles:
-    - role: yatesr.timezone
-      become: True
+# Timezone configuration has moved to time.yml.
+# This will be removed in the Xena release.
+
+# NOTE(wszumski): Making this a non-empty playbook has the benefit of
+# silencing the tox syntax check which doesn't like empty playbooks.
+
+- hosts: localhost
+  tasks:
+    - name: Warn about deprecation of this playbook
+      fail:
+        msg: |
+          This playbook has been deprecated, please use time.yml instead.
+          Kayobe should not run this playbook, so if you are seeing this
+          message then either something has gone wrong, or you are trying
+          to run it manually. This playbook will be removed in the Xena
+          release.
+      # NOTE(wszumski): We want this to print a nice big red warning and
+      # not to fail the run.
+      ignore_errors: yes
+
+- import_playbook: time.yml
diff --git a/doc/source/configuration/reference/hosts.rst b/doc/source/configuration/reference/hosts.rst
index 1625a9f4c228495a821ba6b983762eea37ff1d3e..86c3c2652f9bfa37cdda5bb9a8bf0473299c381e 100644
--- a/doc/source/configuration/reference/hosts.rst
+++ b/doc/source/configuration/reference/hosts.rst
@@ -406,23 +406,48 @@ timezone. For example:
 
 NTP
 ===
+*tags:*
+  | ``ntp``
 
-Since the Ussuri release, Kayobe no longer supports configuration of an NTP
-daemon on the host, since the ``ntp`` package is no longer available in CentOS
-8.
+Kayobe will configure `Chrony <https://chrony.tuxfamily.org/>`__ on all hosts in the
+``ntp`` group. The default hosts in this group are::
 
-Kolla Ansible can deploy a chrony container on overcloud hosts, and from the
-Ussuri release chrony is enabled by default. There is no support for running a
-chrony container on the seed or seed hypervisor hosts.
+.. code-block:: console
 
-To disable the containerised chrony daemon, set the following in
-``${KAYOBE_CONFIG_PATH}/kolla.yml``:
+    [ntp:children]
+    # Kayobe will configure Chrony on members of this group.
+    seed
+    seed-hypervisor
+    overcloud
 
-.. code-block:: yaml
+This provides a flexible way to opt in or out of having kayobe manage
+the NTP service.
+
+Variables
+---------
 
-   kolla_enable_chrony: false
+Network Time Protocol (NTP) may be configured via variables in
+``${KAYOBE_CONFIG_PATH}/time.yml``. The list of NTP servers is
+configured via ``chrony_ntp_servers``, and by default the ``pool.ntp.org``
+servers are used.
+
+Internally, kayobe uses the the `mrlesmithjr.chrony
+<https://galaxy.ansible.com/mrlesmithjr/chrony>`__ Ansible role. Rather than
+maintain a mapping between the ``kayobe`` and ``mrlesmithjr.chrony`` worlds, all
+variables are simply passed through. This means you can use all variables that
+the role defines. For example to change ``chrony_maxupdateskew`` and override
+the kayobe defaults for ``chrony_ntp_servers``:
+
+.. code-block:: yaml
+   :caption: ``time.yml``
 
-.. _configuration-hosts-mdadm:
+   chrony_ntp_servers:
+     - server: 0.debian.pool.ntp.org
+       options:
+         - option: iburst
+         - option: minpoll
+           val: 8
+   chrony_maxupdateskew: 150.0
 
 Software RAID
 =============
diff --git a/etc/kayobe/inventory/groups b/etc/kayobe/inventory/groups
index a009693e3a6ace72f8c2a9c387a94ea744ec8059..fa1ced47ce52301655548e489427f1dffc312ad0 100644
--- a/etc/kayobe/inventory/groups
+++ b/etc/kayobe/inventory/groups
@@ -42,7 +42,7 @@ storage
 compute
 
 ###############################################################################
-# Docker groups.
+# Service groups.
 
 [docker:children]
 # Hosts in this group will have Docker installed.
@@ -59,6 +59,12 @@ compute
 # registries which may become unsynchronized.
 seed
 
+[ntp:children]
+# Kayobe will configure Chrony on members of this group.
+seed
+seed-hypervisor
+overcloud
+
 ###############################################################################
 # Baremetal compute node groups.
 
diff --git a/etc/kayobe/time.yml b/etc/kayobe/time.yml
index c0a86d7c772c25664e344ba93241ada675ef99af..f5a0cbbfd3dfa09d8bee99ee03e525c54cfaedd3 100644
--- a/etc/kayobe/time.yml
+++ b/etc/kayobe/time.yml
@@ -7,6 +7,32 @@
 # Name of the local timezone.
 #timezone:
 
+###############################################################################
+# Network Time Protocol (NTP).
+
+# Kayobe default time sources
+#chrony_ntp_servers_default:
+
+# List of NTP time sources to configure. Format is a list of dictionaries with
+# the following keys:
+# server:  host or pool
+# type:    (Optional) Defaults to server. Maps to a time source in the
+#           configuration file. Can be one of server, peer, pool.
+# options: (Optional) List of options that depends on type, see Chrony
+#          documentation for details.
+# See: https://chrony.tuxfamily.org/doc/4.0/chrony.conf.html
+#
+# Example of configuring a pool and customising the pool specific maxsources
+# option:
+# chrony_ntp_servers:
+#   - server: pool.ntp.org
+#     type: pool
+#     options:
+#       - option: maxsources
+#         val: 3
+#
+#chrony_ntp_servers:
+
 ###############################################################################
 # 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 8ff034f8bfc54617efd1c55d175abcf0b4a1520a..6b91b7881b15ff66da9e25de78aa89f8d687d778 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -414,7 +414,7 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin,
     * Configure user accounts, group associations, and authorised SSH keys.
     * Configure the host's network interfaces.
     * Set sysctl parameters.
-    * Configure timezone.
+    * Configure timezone and ntp.
     * Optionally, configure software RAID arrays.
     * Optionally, configure encryption.
     * Configure LVM volumes.
@@ -453,7 +453,7 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin,
         if parsed_args.wipe_disks:
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
-            "users", "dev-tools", "network", "sysctl", "timezone",
+            "users", "dev-tools", "network", "sysctl", "time",
             "mdadm", "luks", "lvm", "seed-hypervisor-libvirt-host")
         self.run_kayobe_playbooks(parsed_args, playbooks,
                                   limit="seed-hypervisor")
@@ -574,7 +574,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
     * Set sysctl parameters.
     * Configure IP routing and source NAT.
     * Disable bootstrap interface configuration.
-    * Configure timezone.
+    * Configure timezone and ntp.
     * Optionally, configure software RAID arrays.
     * Optionally, configure encryption.
     * Configure LVM volumes.
@@ -608,7 +608,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
             "users", "dev-tools", "disable-selinux", "network",
-            "sysctl", "ip-routing", "snat", "disable-glean", "timezone",
+            "sysctl", "ip-routing", "snat", "disable-glean", "time",
             "mdadm", "luks", "lvm", "docker-devicemapper",
             "kolla-ansible-user", "kolla-pip", "kolla-target-venv")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
@@ -948,7 +948,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
     * Configure the host's network interfaces.
     * Set sysctl parameters.
     * Disable bootstrap interface configuration.
-    * Configure timezone.
+    * Configure timezone and ntp.
     * Optionally, configure software RAID arrays.
     * Optionally, configure encryption.
     * Configure LVM volumes.
@@ -981,7 +981,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
             playbooks += _build_playbook_list("wipe-disks")
         playbooks += _build_playbook_list(
             "users", "dev-tools", "disable-selinux", "network",
-            "sysctl", "disable-glean", "disable-cloud-init", "timezone",
+            "sysctl", "disable-glean", "disable-cloud-init", "time",
             "mdadm", "luks", "lvm", "docker-devicemapper",
             "kolla-ansible-user", "kolla-pip", "kolla-target-venv")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index e4b3dcce124b44fd03ceb3b68dde80835b2c6270..988bc2642636cf264cfc90b522006b9369d0fc28 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -325,7 +325,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "dev-tools.yml"),
                     utils.get_data_files_path("ansible", "network.yml"),
                     utils.get_data_files_path("ansible", "sysctl.yml"),
-                    utils.get_data_files_path("ansible", "timezone.yml"),
+                    utils.get_data_files_path("ansible", "time.yml"),
                     utils.get_data_files_path("ansible", "mdadm.yml"),
                     utils.get_data_files_path("ansible", "luks.yml"),
                     utils.get_data_files_path("ansible", "lvm.yml"),
@@ -500,7 +500,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ip-routing.yml"),
                     utils.get_data_files_path("ansible", "snat.yml"),
                     utils.get_data_files_path("ansible", "disable-glean.yml"),
-                    utils.get_data_files_path("ansible", "timezone.yml"),
+                    utils.get_data_files_path("ansible", "time.yml"),
                     utils.get_data_files_path("ansible", "mdadm.yml"),
                     utils.get_data_files_path("ansible", "luks.yml"),
                     utils.get_data_files_path("ansible", "lvm.yml"),
@@ -1045,7 +1045,7 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "disable-glean.yml"),
                     utils.get_data_files_path(
                         "ansible", "disable-cloud-init.yml"),
-                    utils.get_data_files_path("ansible", "timezone.yml"),
+                    utils.get_data_files_path("ansible", "time.yml"),
                     utils.get_data_files_path("ansible", "mdadm.yml"),
                     utils.get_data_files_path("ansible", "luks.yml"),
                     utils.get_data_files_path("ansible", "lvm.yml"),
diff --git a/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2 b/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
index 39b1b6c81663d1b150b3b881be30742c7809a890..791c3875617bf799ac290233efa8e41706196d37 100644
--- a/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
+++ b/playbooks/kayobe-overcloud-host-configure-base/overrides.yml.j2
@@ -122,3 +122,11 @@ dnf_custom_repos:
 # Enable DNF Automatic.
 dnf_automatic_enabled: true
 {% endif %}
+
+# Override the default NTP pool
+chrony_ntp_servers:
+  - server: time.cloudflare.com
+    type: pool
+    options:
+      - option: maxsources
+        val: 2
diff --git a/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py b/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
index 58654cf90ba81be21706f5e25eeccf08522c0a71..a6662bcd73a390f24a23d178ea0c7195f79f7f4b 100644
--- a/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
+++ b/playbooks/kayobe-overcloud-host-configure-base/tests/test_overcloud_host_configure.py
@@ -134,6 +134,43 @@ def test_timezone(host):
     assert "Pacific/Honolulu" in status
 
 
+def test_ntp_alternative_services_disabled(host):
+    # Tests that we don't have any conflicting NTP servers running
+    # NOTE(wszumski): We always mask services even if they don't exist
+    ntpd_service = host.service("ntp")
+    assert ntpd_service.is_masked
+    assert not ntpd_service.is_running
+
+    timesyncd_service = host.service("systemd-timesyncd")
+    assert timesyncd_service.is_masked
+    assert not timesyncd_service.is_running
+
+
+def test_ntp_running(host):
+    # Tests that NTP services are enabled and running
+    assert host.package("chrony").is_installed
+    assert host.service("chronyd").is_enabled
+    assert host.service("chronyd").is_running
+
+
+def test_ntp_non_default_time_server(host):
+    # Tests that the NTP pool has been changed from pool.ntp.org to
+    # time.cloudflare.com
+    if 'centos' in host.system_info.distribution.lower():
+        chrony_config = host.file("/etc/chrony.conf")
+    else:
+        # Debian based distributions use the following path
+        chrony_config = host.file("/etc/chrony/chrony.conf")
+    assert chrony_config.exists
+    assert "time.cloudflare.com" in chrony_config.content_string
+
+
+def test_ntp_clock_synchronized(host):
+    # Tests that the clock is synchronized
+    status_output = host.check_output("timedatectl status")
+    assert "synchronized: yes" in status_output
+
+
 @pytest.mark.parametrize('repo', ["AppStream", "BaseOS", "Extras", "epel",
                                   "epel-modular"])
 @pytest.mark.skipif(not _is_dnf(), reason="DNF only supported on CentOS 8")
diff --git a/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml b/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..acf6587efdc84c364e55737ec74d032f54b35d6f
--- /dev/null
+++ b/releasenotes/notes/install-chrony-on-the-host-283fa6fdd9cf9b4c.yaml
@@ -0,0 +1,11 @@
+---
+upgrade:
+  - |
+    Updates the NTP implementation from the chrony container deployed by
+    kolla-ansible to configuring chrony as a host service.  Chrony is now
+    installed on all hosts in the ``ntp`` group, which defaults to include
+    the seed, overcloud, and seed-hypervisor groups. On existing deployments,
+    you should run `kayobe overcloud host configure` to migrate from the
+    kolla-ansible deployed container. This can optionally be scoped to just
+    use the ``ntp`` tag. You can continue to use the kolla container by
+    setting `kolla_enable_chrony` to ``true``.
diff --git a/requirements.yml b/requirements.yml
index b879dde1826c7ce80315ff2043a98209048f3187..1d7a5ca488afc600daa9d217aa2efcfb14feccc0 100644
--- a/requirements.yml
+++ b/requirements.yml
@@ -8,6 +8,8 @@
   version: 8438592c84585c86e62ae07e526d3da53629b377
 - src: MichaelRigart.interfaces
   version: v1.11.1
+- src: mrlesmithjr.chrony
+  version: v0.1.0
 - src: mrlesmithjr.manage-lvm
   version: v0.1.4
 - src: mrlesmithjr.mdadm