Support encryption of configuration using Ansible Vault
This commit is contained in:
parent
cb7ed2f48c
commit
f06483eb68
@ -102,6 +102,18 @@ Site Localisation and Customisation
|
|||||||
Site localisation and customisation is applied using Ansible extra-vars files
|
Site localisation and customisation is applied using Ansible extra-vars files
|
||||||
in ``${KAYOBE_CONFIG_PATH}/*.yml``.
|
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
|
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) $ kayobe complete > kayobe-complete
|
||||||
(kayobe-venv) $ source 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
|
Ansible Control Host
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
@ -27,12 +27,34 @@ DEFAULT_CONFIG_PATH = "/etc/kayobe"
|
|||||||
|
|
||||||
CONFIG_PATH_ENV = "KAYOBE_CONFIG_PATH"
|
CONFIG_PATH_ENV = "KAYOBE_CONFIG_PATH"
|
||||||
|
|
||||||
|
VAULT_PASSWORD_ENV = "KAYOBE_VAULT_PASSWORD"
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
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):
|
def add_args(parser):
|
||||||
"""Add arguments required for running Ansible playbooks to a parser."""
|
"""Add arguments required for running Ansible playbooks to a parser."""
|
||||||
default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)
|
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",
|
parser.add_argument("-b", "--become", action="store_true",
|
||||||
help="run operations with become (nopasswd implied)")
|
help="run operations with become (nopasswd implied)")
|
||||||
parser.add_argument("-C", "--check", action="store_true",
|
parser.add_argument("-C", "--check", action="store_true",
|
||||||
@ -57,6 +79,9 @@ def add_args(parser):
|
|||||||
parser.add_argument("-t", "--tags", metavar="TAGS",
|
parser.add_argument("-t", "--tags", metavar="TAGS",
|
||||||
help="only run plays and tasks tagged with these "
|
help="only run plays and tasks tagged with these "
|
||||||
"values")
|
"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):
|
def _get_inventory_path(parsed_args):
|
||||||
@ -108,6 +133,10 @@ def build_args(parsed_args, playbooks,
|
|||||||
cmd = ["ansible-playbook"]
|
cmd = ["ansible-playbook"]
|
||||||
if verbose_level:
|
if verbose_level:
|
||||||
cmd += ["-" + "v" * 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)
|
inventory = _get_inventory_path(parsed_args)
|
||||||
cmd += ["--inventory", inventory]
|
cmd += ["--inventory", inventory]
|
||||||
vars_files = _get_vars_files(parsed_args.config_path)
|
vars_files = _get_vars_files(parsed_args.config_path)
|
||||||
|
26
kayobe/cmd/kayobe_vault_password_helper.py
Normal file
26
kayobe/cmd/kayobe_vault_password_helper.py
Normal 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)
|
@ -94,6 +94,7 @@ class TestCase(unittest.TestCase):
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
ansible.add_args(parser)
|
ansible.add_args(parser)
|
||||||
args = [
|
args = [
|
||||||
|
"--ask-vault-pass",
|
||||||
"--become",
|
"--become",
|
||||||
"--check",
|
"--check",
|
||||||
"--config-path", "/path/to/config",
|
"--config-path", "/path/to/config",
|
||||||
@ -106,6 +107,7 @@ class TestCase(unittest.TestCase):
|
|||||||
ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
|
ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
|
||||||
expected_cmd = [
|
expected_cmd = [
|
||||||
"ansible-playbook",
|
"ansible-playbook",
|
||||||
|
"--ask-vault-pass",
|
||||||
"--inventory", "/path/to/inventory",
|
"--inventory", "/path/to/inventory",
|
||||||
"-e", "@/path/to/config/vars-file1.yml",
|
"-e", "@/path/to/config/vars-file1.yml",
|
||||||
"-e", "@/path/to/config/vars-file2.yaml",
|
"-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_run.assert_called_once_with(expected_cmd, quiet=False)
|
||||||
mock_vars.assert_called_once_with("/path/to/config")
|
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(utils, "run_command")
|
||||||
@mock.patch.object(ansible, "_get_vars_files")
|
@mock.patch.object(ansible, "_get_vars_files")
|
||||||
@mock.patch.object(ansible, "_validate_args")
|
@mock.patch.object(ansible, "_validate_args")
|
||||||
|
3
setup.py
3
setup.py
@ -48,7 +48,8 @@ setup(
|
|||||||
|
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'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': [
|
'kayobe.cli': [
|
||||||
'control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap',
|
'control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap',
|
||||||
|
Loading…
Reference in New Issue
Block a user