Skip to content

Commit

Permalink
Ansible vault: a framework for encrypting any playbook or var file.
Browse files Browse the repository at this point in the history
  • Loading branch information
jctanner committed Feb 18, 2014
1 parent 30611ea commit 427b8dc
Show file tree
Hide file tree
Showing 10 changed files with 724 additions and 34 deletions.
8 changes: 6 additions & 2 deletions bin/ansible-playbook
Expand Up @@ -62,6 +62,8 @@ def main(args):
check_opts=True,
diff_opts=True
)
#parser.add_option('--vault-password', dest="vault_password",
# help="password for vault encrypted files")

This comment has been minimized.

Copy link
@fesplugas

fesplugas Feb 19, 2014

Contributor

Is this option going to be enabled? We have a CI server deploying our application with ansible and we need a way to provide a password in order to decrypt the encrypted files.

This comment has been minimized.

Copy link
@bcoca

bcoca via email Feb 19, 2014

Member

This comment has been minimized.

Copy link
@fesplugas

fesplugas Feb 20, 2014

Contributor

We are using TravisCI, and it's not possible to actively provide a password to decrypt the vault.

parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
help="set additional variables as key=value or YAML/JSON", default=[])
parser.add_option('-t', '--tags', dest='tags', default='all',
Expand Down Expand Up @@ -100,12 +102,13 @@ def main(args):
su_pass = None
if not options.listhosts and not options.syntax and not options.listtasks:
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
options.ask_vault_pass = options.ask_vault_pass or C.DEFAULT_ASK_VAULT_PASS
# Never ask for an SSH password when we run with local connection
if options.connection == "local":
options.ask_pass = False
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS
(sshpass, sudopass, su_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass)
(sshpass, sudopass, su_pass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass, ask_vault_pass=options.ask_vault_pass)
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
options.su_user = options.su_user or C.DEFAULT_SU_USER

Expand Down Expand Up @@ -170,7 +173,8 @@ def main(args):
diff=options.diff,
su=options.su,
su_pass=su_pass,
su_user=options.su_user
su_user=options.su_user,
vault_password=vault_pass
)

if options.listhosts or options.listtasks or options.syntax:
Expand Down
187 changes: 187 additions & 0 deletions bin/ansible-vault
@@ -0,0 +1,187 @@
#!/usr/bin/env python

# (c) 2014, James Tanner <tanner.jc@gmail.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# ansible-pull is a script that runs ansible in local mode
# after checking out a playbooks directory from source repo. There is an
# example playbook to bootstrap this script in the examples/ dir which
# installs ansible and sets it up to run on cron.

import sys
import traceback

from ansible import utils
from ansible import errors
from ansible.utils.vault import *
from ansible.utils.vault import Vault

from optparse import OptionParser

#-------------------------------------------------------------------------------------
# Utility functions for parsing actions/options
#-------------------------------------------------------------------------------------

VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey")

def build_option_parser(action):
"""
Builds an option parser object based on the action
the user wants to execute.
"""

usage = "usage: %%prog [%s] [--help] [options] file_name" % "|".join(VALID_ACTIONS)
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
OptionParser.format_epilog = lambda self, formatter: self.epilog
parser = OptionParser(usage=usage, epilog=epilog)

if not action:
parser.print_help()
sys.exit()

# options for all actions
#parser.add_option('-p', '--password', help="encryption key")
#parser.add_option('-c', '--cipher', dest='cipher', default="AES", help="cipher to use")
parser.add_option('-d', '--debug', dest='debug', action="store_true", help="debug")

# options specific to actions
if action == "create":
parser.set_usage("usage: %prog create [options] file_name")
elif action == "decrypt":
parser.set_usage("usage: %prog decrypt [options] file_name")
elif action == "edit":
parser.set_usage("usage: %prog edit [options] file_name")
elif action == "encrypt":
parser.set_usage("usage: %prog encrypt [options] file_name")
elif action == "rekey":
parser.set_usage("usage: %prog rekey [options] file_name")

# done, return the parser
return parser

def get_action(args):
"""
Get the action the user wants to execute from the
sys argv list.
"""
for i in range(0,len(args)):
arg = args[i]
if arg in VALID_ACTIONS:
del args[i]
return arg
return None

def get_opt(options, k, defval=""):
"""
Returns an option from an Optparse values instance.
"""
try:
data = getattr(options, k)
except:
return defval
if k == "roles_path":
if os.pathsep in data:
data = data.split(os.pathsep)[0]
return data

#-------------------------------------------------------------------------------------
# Command functions
#-------------------------------------------------------------------------------------

def _get_vault(filename, options, password):
this_vault = Vault()
this_vault.filename = filename
this_vault.vault_password = password
this_vault.password = password
return this_vault

def execute_create(args, options, parser):

if len(args) > 1:
raise errors.AnsibleError("create does not accept more than one filename")

password, new_password = utils.ask_vaultpasswords(ask_vault_pass=True, confirm_vault=True)

this_vault = _get_vault(args[0], options, password)
if not hasattr(options, 'cipher'):
this_vault.cipher = 'AES'
this_vault.create()

def execute_decrypt(args, options, parser):

password, new_password = utils.ask_vaultpasswords(ask_vault_pass=True)

for f in args:
this_vault = _get_vault(f, options, password)
this_vault.decrypt()

print "Decryption successful"

def execute_edit(args, options, parser):

if len(args) > 1:
raise errors.AnsibleError("create does not accept more than one filename")

password, new_password = utils.ask_vaultpasswords(ask_vault_pass=True)

for f in args:
this_vault = _get_vault(f, options, password)
this_vault.edit()

def execute_encrypt(args, options, parser):

password, new_password = utils.ask_vaultpasswords(ask_vault_pass=True, confirm_vault=True)

for f in args:
this_vault = _get_vault(f, options, password)
if not hasattr(options, 'cipher'):
this_vault.cipher = 'AES'
this_vault.encrypt()

print "Encryption successful"

def execute_rekey(args, options, parser):

password, new_password = utils.ask_vaultpasswords(ask_vault_pass=True, ask_new_vault_pass=True, confirm_new=True)

for f in args:
this_vault = _get_vault(f, options, password)
this_vault.rekey(new_password)

print "Rekey successful"

#-------------------------------------------------------------------------------------
# MAIN
#-------------------------------------------------------------------------------------

def main():

action = get_action(sys.argv)
parser = build_option_parser(action)
(options, args) = parser.parse_args()

# execute the desired action
try:
fn = globals()["execute_%s" % action]
fn(args, options, parser)
except Exception, err:
if options.debug:
print traceback.format_exc()
print "ERROR:",err
sys.exit(1)

if __name__ == "__main__":
main()

4 changes: 3 additions & 1 deletion lib/ansible/constants.py
Expand Up @@ -117,6 +117,7 @@ def shell_expand_path(path):
DEFAULT_SUDO_USER = get_config(p, DEFAULTS, 'sudo_user', 'ANSIBLE_SUDO_USER', 'root')
DEFAULT_ASK_SUDO_PASS = get_config(p, DEFAULTS, 'ask_sudo_pass', 'ANSIBLE_ASK_SUDO_PASS', False, boolean=True)
DEFAULT_REMOTE_PORT = get_config(p, DEFAULTS, 'remote_port', 'ANSIBLE_REMOTE_PORT', None, integer=True)
DEFAULT_ASK_VAULT_PASS = get_config(p, DEFAULTS, 'ask_vault_pass', 'ANSIBLE_ASK_VAULT_PASS', False, boolean=True)
DEFAULT_TRANSPORT = get_config(p, DEFAULTS, 'transport', 'ANSIBLE_TRANSPORT', 'smart')
DEFAULT_SCP_IF_SSH = get_config(p, 'ssh_connection', 'scp_if_ssh', 'ANSIBLE_SCP_IF_SSH', False, boolean=True)
DEFAULT_MANAGED_STR = get_config(p, DEFAULTS, 'ansible_managed', None, 'Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}')
Expand Down Expand Up @@ -172,4 +173,5 @@ def shell_expand_path(path):
DEFAULT_REMOTE_PASS = None
DEFAULT_SUBSET = None
DEFAULT_SU_PASS = None

VAULT_VERSION_MIN = 1.0
VAULT_VERSION_MAX = 1.0
8 changes: 4 additions & 4 deletions lib/ansible/inventory/__init__.py
Expand Up @@ -347,19 +347,19 @@ def _get_group_variables(self, groupname):
raise Exception("group not found: %s" % groupname)
return group.get_variables()

def get_variables(self, hostname):
def get_variables(self, hostname, vault_password=None):
if hostname not in self._vars_per_host:
self._vars_per_host[hostname] = self._get_variables(hostname)
self._vars_per_host[hostname] = self._get_variables(hostname, vault_password=vault_password)
return self._vars_per_host[hostname]

def _get_variables(self, hostname):
def _get_variables(self, hostname, vault_password=None):

host = self.get_host(hostname)
if host is None:
raise errors.AnsibleError("host not found: %s" % hostname)

vars = {}
vars_results = [ plugin.run(host) for plugin in self._vars_plugins ]
vars_results = [ plugin.run(host, vault_password=vault_password) for plugin in self._vars_plugins ]
for updated in vars_results:
if updated is not None:
vars.update(updated)
Expand Down
14 changes: 7 additions & 7 deletions lib/ansible/inventory/vars_plugins/group_vars.py
Expand Up @@ -23,7 +23,7 @@
from ansible import utils
import ansible.constants as C

def _load_vars(basepath, results):
def _load_vars(basepath, results, vault_password=None):
"""
Load variables from any potential yaml filename combinations of basepath,
returning result.
Expand All @@ -35,7 +35,7 @@ def _load_vars(basepath, results):
found_paths = []

for path in paths_to_check:
found, results = _load_vars_from_path(path, results)
found, results = _load_vars_from_path(path, results, vault_password=vault_password)
if found:
found_paths.append(path)

Expand All @@ -49,7 +49,7 @@ def _load_vars(basepath, results):

return results

def _load_vars_from_path(path, results):
def _load_vars_from_path(path, results, vault_password=None):
"""
Robustly access the file at path and load variables, carefully reporting
errors in a friendly/informative way.
Expand Down Expand Up @@ -90,7 +90,7 @@ def _load_vars_from_path(path, results):

# regular file
elif stat.S_ISREG(pathstat.st_mode):
data = utils.parse_yaml_from_file(path)
data = utils.parse_yaml_from_file(path, vault_password=vault_password)
if type(data) != dict:
raise errors.AnsibleError(
"%s must be stored as a dictionary/hash" % path)
Expand Down Expand Up @@ -143,7 +143,7 @@ def __init__(self, inventory):

self.inventory = inventory

def run(self, host):
def run(self, host, vault_password=None):

""" main body of the plugin, does actual loading """

Expand Down Expand Up @@ -183,11 +183,11 @@ def run(self, host):
# load vars in dir/group_vars/name_of_group
for group in groups:
base_path = os.path.join(basedir, "group_vars/%s" % group)
results = _load_vars(base_path, results)
results = _load_vars(base_path, results, vault_password=vault_password)

# same for hostvars in dir/host_vars/name_of_host
base_path = os.path.join(basedir, "host_vars/%s" % host.name)
results = _load_vars(base_path, results)
results = _load_vars(base_path, results, vault_password=vault_password)

# all done, results is a dictionary of variables for this particular host.
return results
Expand Down
11 changes: 7 additions & 4 deletions lib/ansible/playbook/__init__.py
Expand Up @@ -72,6 +72,7 @@ def __init__(self,
su = False,
su_user = False,
su_pass = False,
vault_password = False,
):

"""
Expand Down Expand Up @@ -138,6 +139,7 @@ def __init__(self,
self.su = su
self.su_user = su_user
self.su_pass = su_pass
self.vault_password = vault_password

self.callbacks.playbook = self
self.runner_callbacks.playbook = self
Expand Down Expand Up @@ -172,7 +174,7 @@ def _load_playbook_from_file(self, path, vars={}):
run top level error checking on playbooks and allow them to include other playbooks.
'''

playbook_data = utils.parse_yaml_from_file(path)
playbook_data = utils.parse_yaml_from_file(path, vault_password=self.vault_password)
accumulated_plays = []
play_basedirs = []

Expand Down Expand Up @@ -242,7 +244,7 @@ def run(self):
# loop through all patterns and run them
self.callbacks.on_start()
for (play_ds, play_basedir) in zip(self.playbook, self.play_basedirs):
play = Play(self, play_ds, play_basedir)
play = Play(self, play_ds, play_basedir, vault_password=self.vault_password)
assert play is not None

matched_tags, unmatched_tags = play.compare_tags(self.only_tags)
Expand Down Expand Up @@ -352,6 +354,7 @@ def _run_task_internal(self, task):
su=task.su,
su_user=task.su_user,
su_pass=task.su_pass,
vault_pass = self.vault_password,
run_hosts=hosts,
no_log=task.no_log,
)
Expand Down Expand Up @@ -504,6 +507,7 @@ def _do_setup_step(self, play):
su=play.su,
su_user=play.su_user,
su_pass=self.su_pass,
vault_pass=self.vault_password,
transport=play.transport,
is_playbook=True,
module_vars=play.vars,
Expand Down Expand Up @@ -569,9 +573,8 @@ def _run_play(self, play):
self._do_setup_step(play)

# now with that data, handle contentional variable file imports!

all_hosts = self._trim_unavailable_hosts(play._play_hosts)
play.update_vars_files(all_hosts)
play.update_vars_files(all_hosts, vault_password=self.vault_password)
hosts_count = len(all_hosts)

serialized_batch = []
Expand Down

0 comments on commit 427b8dc

Please sign in to comment.