diff --git a/kayobe/ansible.py b/kayobe/ansible.py
index 6fbaa46b14079afd719dcb0ebd049d5dd5b43e67..009c72c5642026d24e1cb6002a14d0f0f2c70bf6 100644
--- a/kayobe/ansible.py
+++ b/kayobe/ansible.py
@@ -109,7 +109,7 @@ def _get_vars_files(config_path):
     vars_files = []
     for vars_file in os.listdir(config_path):
         abs_path = os.path.join(config_path, vars_file)
-        if utils.is_readable_file(abs_path):
+        if utils.is_readable_file(abs_path)["result"]:
             root, ext = os.path.splitext(vars_file)
             if ext in (".yml", ".yaml", ".json"):
                 vars_files.append(abs_path)
@@ -277,3 +277,10 @@ def prune_galaxy_roles(parsed_args):
     ]
     LOG.debug("Removing roles: %s", ",".join(roles_to_remove))
     utils.galaxy_remove(roles_to_remove, "ansible/roles")
+
+
+def passwords_yml_exists(parsed_args):
+    """Return whether passwords.yml exists in the kayobe configuration."""
+    passwords_path = os.path.join(parsed_args.config_path,
+                                  'kolla', 'passwords.yml')
+    return utils.is_readable_file(passwords_path)["result"]
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 4998560cd57de3ad6448596b3c6fac3e826409e6..02780a2e64d9ed3dfa21cb342ea9049a21053662 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -144,12 +144,15 @@ class KollaAnsibleMixin(object):
         return kolla_ansible.run_seed(*args, **kwargs)
 
 
-class ControlHostBootstrap(KayobeAnsibleMixin, VaultMixin, Command):
+class ControlHostBootstrap(KayobeAnsibleMixin, KollaAnsibleMixin, VaultMixin,
+                           Command):
     """Bootstrap the Kayobe control environment.
 
     * Downloads and installs Ansible roles from Galaxy.
     * Generates an SSH key for the Ansible control host, if one does not exist.
     * Installs kolla-ansible on the Ansible control host.
+    * Generates admin-openrc.sh and public-openrc.sh files when passwords.yml
+      exists.
     """
 
     def take_action(self, parsed_args):
@@ -157,10 +160,33 @@ class ControlHostBootstrap(KayobeAnsibleMixin, VaultMixin, Command):
         ansible.install_galaxy_roles(parsed_args)
         playbooks = _build_playbook_list("bootstrap")
         self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True)
+
+        passwords_exist = ansible.passwords_yml_exists(parsed_args)
+        if passwords_exist:
+            # Install and generate configuration - necessary for post-deploy.
+            ka_tags = None
+        else:
+            ka_tags = "install"
         playbooks = _build_playbook_list("kolla-ansible")
-        self.run_kayobe_playbooks(parsed_args, playbooks, tags="install",
+        self.run_kayobe_playbooks(parsed_args, playbooks, tags=ka_tags,
                                   ignore_limit=True)
 
+        if passwords_exist:
+            # If we are bootstrapping a control host for an existing
+            # environment, we should also generate the admin-openrc.sh and
+            # public-openrc.sh scripts that provide admin credentials.
+
+            # FIXME: Fudge to work around incorrect configuration path.
+            extra_vars = {"node_config_directory":
+                          parsed_args.kolla_config_path}
+            self.run_kolla_ansible_overcloud(parsed_args, "post-deploy",
+                                             extra_vars=extra_vars)
+            # Create an environment file for accessing the public API as the
+            # admin user.
+            playbooks = _build_playbook_list("public-openrc")
+            self.run_kayobe_playbooks(parsed_args, playbooks,
+                                      ignore_limit=True)
+
 
 class ControlHostUpgrade(KayobeAnsibleMixin, VaultMixin, Command):
     """Upgrade the Kayobe control environment.
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index 7e86c1e832ffc8c8ff48b29746f1264dbba52911..458f14b42b6cafdc228fbe1b5627967fcd619bd4 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -35,9 +35,12 @@ class TestApp(cliff.app.App):
 class TestCase(unittest.TestCase):
 
     @mock.patch.object(ansible, "install_galaxy_roles", autospec=True)
+    @mock.patch.object(ansible, "passwords_yml_exists", autospec=True)
     @mock.patch.object(commands.KayobeAnsibleMixin,
                        "run_kayobe_playbooks")
-    def test_control_host_bootstrap(self, mock_run, mock_install):
+    def test_control_host_bootstrap(self, mock_run, mock_passwords,
+                                    mock_install):
+        mock_passwords.return_value = False
         command = commands.ControlHostBootstrap(TestApp(), [])
         parser = command.get_parser("test")
         parsed_args = parser.parse_args([])
@@ -59,6 +62,50 @@ class TestCase(unittest.TestCase):
         ]
         self.assertEqual(expected_calls, mock_run.call_args_list)
 
+    @mock.patch.object(ansible, "install_galaxy_roles", autospec=True)
+    @mock.patch.object(ansible, "passwords_yml_exists", autospec=True)
+    @mock.patch.object(commands.KayobeAnsibleMixin,
+                       "run_kayobe_playbooks")
+    @mock.patch.object(commands.KollaAnsibleMixin,
+                       "run_kolla_ansible_overcloud")
+    def test_control_host_bootstrap_with_passwords(
+            self, mock_kolla_run, mock_run, mock_passwords, mock_install):
+        mock_passwords.return_value = True
+        command = commands.ControlHostBootstrap(TestApp(), [])
+        parser = command.get_parser("test")
+        parsed_args = parser.parse_args([])
+        result = command.run(parsed_args)
+        self.assertEqual(0, result)
+        mock_install.assert_called_once_with(parsed_args)
+        expected_calls = [
+            mock.call(
+                mock.ANY,
+                [utils.get_data_files_path("ansible", "bootstrap.yml")],
+                ignore_limit=True
+            ),
+            mock.call(
+                mock.ANY,
+                [utils.get_data_files_path("ansible", "kolla-ansible.yml")],
+                tags=None,
+                ignore_limit=True
+            ),
+            mock.call(
+                mock.ANY,
+                [utils.get_data_files_path("ansible", "public-openrc.yml")],
+                ignore_limit=True
+            ),
+        ]
+        self.assertEqual(expected_calls, mock_run.call_args_list)
+
+        expected_calls = [
+            mock.call(
+                mock.ANY,
+                "post-deploy",
+                extra_vars={"node_config_directory": "/etc/kolla"},
+            )
+        ]
+        self.assertEqual(expected_calls, mock_kolla_run.call_args_list)
+
     @mock.patch.object(ansible, "install_galaxy_roles", autospec=True)
     @mock.patch.object(ansible, "prune_galaxy_roles", autospec=True)
     @mock.patch.object(commands.KayobeAnsibleMixin,
diff --git a/kayobe/tests/unit/test_ansible.py b/kayobe/tests/unit/test_ansible.py
index 95e329571efca5e49107a2fcf2f2ce368d28dc44..86361f474d8eddafa42943ae6f28c44d5da743bb 100644
--- a/kayobe/tests/unit/test_ansible.py
+++ b/kayobe/tests/unit/test_ansible.py
@@ -445,3 +445,29 @@ class TestCase(unittest.TestCase):
         ]
         mock_remove.assert_called_once_with(expected_roles,
                                             "ansible/roles")
+
+    @mock.patch.object(utils, 'is_readable_file', autospec=True)
+    def test_passwords_yml_exists_false(self, mock_is_readable):
+        parser = argparse.ArgumentParser()
+        ansible.add_args(parser)
+        parsed_args = parser.parse_args([])
+        mock_is_readable.return_value = {"result": False}
+
+        result = ansible.passwords_yml_exists(parsed_args)
+
+        self.assertFalse(result)
+        mock_is_readable.assert_called_once_with(
+            "/etc/kayobe/kolla/passwords.yml")
+
+    @mock.patch.object(utils, 'is_readable_file', autospec=True)
+    def test_passwords_yml_exists_true(self, mock_is_readable):
+        parser = argparse.ArgumentParser()
+        ansible.add_args(parser)
+        parsed_args = parser.parse_args(["--config-path", "/path/to/config"])
+        mock_is_readable.return_value = {"result": True}
+
+        result = ansible.passwords_yml_exists(parsed_args)
+
+        self.assertTrue(result)
+        mock_is_readable.assert_called_once_with(
+            "/path/to/config/kolla/passwords.yml")
diff --git a/releasenotes/notes/bootstrap-openrc-9aec3d53d0d62c81.yaml b/releasenotes/notes/bootstrap-openrc-9aec3d53d0d62c81.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..df2e6b834e43ed6c5435ec09aec11c8bcce897de
--- /dev/null
+++ b/releasenotes/notes/bootstrap-openrc-9aec3d53d0d62c81.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+  - |
+    Fixes an issue where the ``admin-openrc.sh`` and ``public-openrc.sh`` files
+    would not be generated when preparing a new control host environment for an
+    existing cloud. These files are now generated during ``kayobe control host
+    bootstrap`` if the Kolla Ansible ``passwords.yml`` file exists in the
+    Kayobe configuration. See `story 2001667
+    <https://storyboard.openstack.org/#!/story/2001667>`__ for details.