From f06483eb6879db89e012d70c23401434b7b589b4 Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Thu, 20 Apr 2017 14:30:02 +0100 Subject: [PATCH] Support encryption of configuration using Ansible Vault --- doc/source/usage.rst | 28 ++++++++++ kayobe/ansible.py | 29 +++++++++++ kayobe/cmd/kayobe_vault_password_helper.py | 26 ++++++++++ kayobe/tests/unit/test_ansible.py | 60 ++++++++++++++++++++++ setup.py | 3 +- 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 kayobe/cmd/kayobe_vault_password_helper.py diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 01bf40a6b..27a832a42 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -102,6 +102,18 @@ Site Localisation and Customisation Site localisation and customisation is applied using Ansible extra-vars files in ``${KAYOBE_CONFIG_PATH}/*.yml``. +Encryption of Secrets +^^^^^^^^^^^^^^^^^^^^^ + +Kayobe supports the use of `Ansible vault +`_ to encrypt sensitive +information in its configuration. The ``ansible-vault`` tool should be used to +manage individual files for which encryption is required. Any of the +configuration files may be encrypted. Since encryption can make working with +Kayobe difficult, it is recommended to follow `best practice +`_, +adding a layer of indirection and using encryption only where necessary. + Command Line Interface ====================== @@ -128,6 +140,22 @@ can be activated by generating and then sourcing the bash completion script:: (kayobe-venv) $ kayobe complete > kayobe-complete (kayobe-venv) $ source kayobe-complete +Working with Ansible Vault +-------------------------- + +If Ansible vault has been used to encrypt Kayobe configuration files, it will +be necessary to provide the ``kayobe`` command with access to vault password. +There are three options for doing this: + +Prompt + Use ``kayobe --ask-vault-pass`` to prompt for the password. +File + Use ``kayobe --vault-password-file `` to read the password from a + (plain text) file. +Environment variable + Export the environment variable ``KAYOBE_VAULT_PASSWORD`` to read the + password from the environment. + Ansible Control Host ==================== diff --git a/kayobe/ansible.py b/kayobe/ansible.py index 0e7a6f792..8b243456d 100644 --- a/kayobe/ansible.py +++ b/kayobe/ansible.py @@ -27,12 +27,34 @@ DEFAULT_CONFIG_PATH = "/etc/kayobe" CONFIG_PATH_ENV = "KAYOBE_CONFIG_PATH" +VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD" + LOG = logging.getLogger(__name__) +def _get_default_vault_password_file(): + """Return the default value for the vault password file argument. + + It is possible to use an environment variable to avoid typing the vault + password. + """ + if not os.getenv(VAULT_PASSWORD_ENV): + return None + cmd = ["which", "kayobe-vault-password-helper"] + try: + output = utils.run_command(cmd, check_output=True) + except subprocess.CalledProcessError: + return None + return output.strip() + + def add_args(parser): """Add arguments required for running Ansible playbooks to a parser.""" default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH) + default_vault_password_file = _get_default_vault_password_file() + vault = parser.add_mutually_exclusive_group() + vault.add_argument("--ask-vault-pass", action="store_true", + help="ask for vault password") parser.add_argument("-b", "--become", action="store_true", help="run operations with become (nopasswd implied)") parser.add_argument("-C", "--check", action="store_true", @@ -57,6 +79,9 @@ def add_args(parser): parser.add_argument("-t", "--tags", metavar="TAGS", help="only run plays and tasks tagged with these " "values") + vault.add_argument("--vault-password-file", metavar="VAULT_PASSWORD_FILE", + default=default_vault_password_file, + help="vault password file") def _get_inventory_path(parsed_args): @@ -108,6 +133,10 @@ def build_args(parsed_args, playbooks, cmd = ["ansible-playbook"] if verbose_level: cmd += ["-" + "v" * verbose_level] + if parsed_args.ask_vault_pass: + cmd += ["--ask-vault-pass"] + elif parsed_args.vault_password_file: + cmd += ["--vault-password-file", parsed_args.vault_password_file] inventory = _get_inventory_path(parsed_args) cmd += ["--inventory", inventory] vars_files = _get_vars_files(parsed_args.config_path) diff --git a/kayobe/cmd/kayobe_vault_password_helper.py b/kayobe/cmd/kayobe_vault_password_helper.py new file mode 100644 index 000000000..0e7ea24b4 --- /dev/null +++ b/kayobe/cmd/kayobe_vault_password_helper.py @@ -0,0 +1,26 @@ +# Copyright (c) 2017 StackHPC Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import print_function +import os + + +VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD" + + +def main(): + """Helper script to allow specification of vault password via env.""" + password = os.getenv(VAULT_PASSWORD_ENV) + if password: + print(password) diff --git a/kayobe/tests/unit/test_ansible.py b/kayobe/tests/unit/test_ansible.py index a8f2681d4..72bb96881 100644 --- a/kayobe/tests/unit/test_ansible.py +++ b/kayobe/tests/unit/test_ansible.py @@ -94,6 +94,7 @@ class TestCase(unittest.TestCase): parser = argparse.ArgumentParser() ansible.add_args(parser) args = [ + "--ask-vault-pass", "--become", "--check", "--config-path", "/path/to/config", @@ -106,6 +107,7 @@ class TestCase(unittest.TestCase): ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"]) expected_cmd = [ "ansible-playbook", + "--ask-vault-pass", "--inventory", "/path/to/inventory", "-e", "@/path/to/config/vars-file1.yml", "-e", "@/path/to/config/vars-file2.yaml", @@ -120,6 +122,64 @@ class TestCase(unittest.TestCase): mock_run.assert_called_once_with(expected_cmd, quiet=False) mock_vars.assert_called_once_with("/path/to/config") + @mock.patch.object(utils, "run_command") + @mock.patch.object(ansible, "_get_vars_files") + @mock.patch.object(ansible, "_validate_args") + def test_run_playbooks_vault_password_file(self, mock_validate, mock_vars, + mock_run): + mock_vars.return_value = [] + parser = argparse.ArgumentParser() + ansible.add_args(parser) + args = [ + "--vault-password-file", "/path/to/vault/pw", + ] + parsed_args = parser.parse_args(args) + ansible.run_playbooks(parsed_args, ["playbook1.yml"]) + expected_cmd = [ + "ansible-playbook", + "--vault-password-file", "/path/to/vault/pw", + "--inventory", "/etc/kayobe/inventory", + "playbook1.yml", + ] + mock_run.assert_called_once_with(expected_cmd, quiet=False) + + @mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass"}) + @mock.patch.object(utils, "run_command") + @mock.patch.object(ansible, "_get_vars_files") + @mock.patch.object(ansible, "_validate_args") + def test_run_playbooks_vault_password_helper(self, mock_validate, + mock_vars, mock_run): + mock_vars.return_value = [] + parser = argparse.ArgumentParser() + mock_run.return_value = "/path/to/kayobe-vault-password-helper" + ansible.add_args(parser) + mock_run.assert_called_once_with( + ["which", "kayobe-vault-password-helper"], check_output=True) + mock_run.reset_mock() + parsed_args = parser.parse_args([]) + ansible.run_playbooks(parsed_args, ["playbook1.yml"]) + expected_cmd = [ + "ansible-playbook", + "--vault-password-file", "/path/to/kayobe-vault-password-helper", + "--inventory", "/etc/kayobe/inventory", + "playbook1.yml", + ] + mock_run.assert_called_once_with(expected_cmd, quiet=False) + + @mock.patch.object(utils, "run_command") + @mock.patch.object(ansible, "_get_vars_files") + @mock.patch.object(ansible, "_validate_args") + def test_run_playbooks_vault_ask_and_file(self, mock_validate, mock_vars, + mock_run): + mock_vars.return_value = [] + parser = argparse.ArgumentParser() + ansible.add_args(parser) + args = [ + "--ask-vault-pass", + "--vault-password-file", "/path/to/vault/pw", + ] + self.assertRaises(SystemExit, parser.parse_args, args) + @mock.patch.object(utils, "run_command") @mock.patch.object(ansible, "_get_vars_files") @mock.patch.object(ansible, "_validate_args") diff --git a/setup.py b/setup.py index 3b1317fc4..3f59b1727 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ setup( entry_points={ 'console_scripts': [ - 'kayobe = kayobe.cmd.kayobe:main' + 'kayobe = kayobe.cmd.kayobe:main', + 'kayobe-vault-password-helper = kayobe.cmd.kayobe_vault_password_helper:main', ], 'kayobe.cli': [ 'control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap',