Support encryption of configuration using Ansible Vault

This commit is contained in:
Mark Goddard 2017-04-20 14:30:02 +01:00
parent cb7ed2f48c
commit f06483eb68
5 changed files with 145 additions and 1 deletions

View File

@ -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
<http://docs.ansible.com/ansible/playbooks_vault.html>`_ 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
<http://docs.ansible.com/ansible/playbooks_best_practices.html#best-practices-for-variables-and-vaults>`_,
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 <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
====================

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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',