summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
authorAustin Hobbs <OxHobbs@users.noreply.github.com>2018-11-06 14:07:28 -0600
committeransibot <ansibot@users.noreply.github.com>2018-11-06 15:07:28 -0500
commit6c94c28a12c31534563a9e59cc7d8ed3fafaa8a7 (patch)
tree81f65a633310df10f9ceeddef2528b38babb2022 /contrib
parente1aa05bf9a1d2e239de4a0967a2d04d8b0b56163 (diff)
downloadansible-6c94c28a12c31534563a9e59cc7d8ed3fafaa8a7.tar.gz
Ansible Vault and Azure Key Vault vault password script (#44544)
* added new vault password files that can be used with Azure Key Vault * fixed pylint errors * fixed pep 8 violations
Diffstat (limited to 'contrib')
-rw-r--r--contrib/vault/azure_vault.ini10
-rwxr-xr-xcontrib/vault/azure_vault.py597
2 files changed, 607 insertions, 0 deletions
diff --git a/contrib/vault/azure_vault.ini b/contrib/vault/azure_vault.ini
new file mode 100644
index 0000000000..d47f976201
--- /dev/null
+++ b/contrib/vault/azure_vault.ini
@@ -0,0 +1,10 @@
+[azure_keyvault] # Used with Azure KeyVault
+vault_name=django-keyvault
+secret_name=vaultpw
+secret_version=9k1e6c7367b33eac8ee241b3698009f3
+
+[azure] # Used by Dynamic Inventory
+group_by_resource_group=yes
+group_by_location=yes
+group_by_security_group=yes
+group_by_tag=yes \ No newline at end of file
diff --git a/contrib/vault/azure_vault.py b/contrib/vault/azure_vault.py
new file mode 100755
index 0000000000..e4b0b847c0
--- /dev/null
+++ b/contrib/vault/azure_vault.py
@@ -0,0 +1,597 @@
+#!/usr/bin/env python
+#
+# This script borrows a great deal of code from the azure_rm.py dynamic inventory script
+# that is packaged with Ansible. This can be found in the Ansible GitHub project at:
+# https://github.com/ansible/ansible/blob/devel/contrib/inventory/azure_rm.py
+#
+# The Azure Dynamic Inventory script was written by:
+# Copyright (c) 2016 Matt Davis, <mdavis@ansible.com>
+# Chris Houseknecht, <house@redhat.com>
+# Altered/Added for Vault functionality:
+# Austin Hobbs, GitHub: @OxHobbs
+
+'''
+Ansible Vault Password with Azure Key Vault Secret Script
+=========================================================
+This script is designed to be used with Ansible Vault. It provides the
+capability to provide this script as the password file to the ansible-vault
+command. This script uses the Azure Python SDK. For instruction on installing
+the Azure Python SDK see http://azure-sdk-for-python.readthedocs.org/
+
+Authentication
+--------------
+The order of precedence is command line arguments, environment variables,
+and finally the [default] profile found in ~/.azure/credentials for all
+authentication parameters.
+
+If using a credentials file, it should be an ini formatted file with one or
+more sections, which we refer to as profiles. The script looks for a
+[default] section, if a profile is not specified either on the command line
+or with an environment variable. The keys in a profile will match the
+list of command line arguments below.
+
+For command line arguments and environment variables specify a profile found
+in your ~/.azure/credentials file, or a service principal or Active Directory
+user.
+
+Command line arguments:
+ - profile
+ - client_id
+ - secret
+ - subscription_id
+ - tenant
+ - ad_user
+ - password
+ - cloud_environment
+ - adfs_authority_url
+ - vault-name
+ - secret-name
+ - secret-version
+
+Environment variables:
+ - AZURE_PROFILE
+ - AZURE_CLIENT_ID
+ - AZURE_SECRET
+ - AZURE_SUBSCRIPTION_ID
+ - AZURE_TENANT
+ - AZURE_AD_USER
+ - AZURE_PASSWORD
+ - AZURE_CLOUD_ENVIRONMENT
+ - AZURE_ADFS_AUTHORITY_URL
+ - AZURE_VAULT_NAME
+ - AZURE_VAULT_SECRET_NAME
+ - AZURE_VAULT_SECRET_VERSION
+
+
+Vault
+-----
+
+The order of precedence of Azure Key Vault Secret information is the same.
+Command line arguments, environment variables, and finally the azure_vault.ini
+file with the [azure_keyvault] section.
+
+azure_vault.ini (or azure_rm.ini if merged with Azure Dynamic Inventory Script)
+------------------------------------------------------------------------------
+As mentioned above, you can control execution using environment variables or a .ini file. A sample
+azure_vault.ini is included. The name of the .ini file is the basename of the inventory script (in this case
+'azure_vault') with a .ini extension. It also assumes the .ini file is alongside the script. To specify
+a different path for the .ini file, define the AZURE_VAULT_INI_PATH environment variable:
+
+ export AZURE_VAULT_INI_PATH=/path/to/custom.ini
+ or
+ export AZURE_VAULT_INI_PATH=[same path as azure_rm.ini if merged]
+
+ __NOTE__: If using the azure_rm.py dynamic inventory script, it is possible to use the same .ini
+ file for both the azure_rm dynamic inventory and the azure_vault password file. Simply add a section
+ named [azure_keyvault] to the ini file with the following properties: vault_name, secret_name and
+ secret_version.
+
+Examples:
+---------
+ Validate the vault_pw script with Python
+ $ python azure_vault.py -n mydjangovault -s vaultpw -v 6b6w7f7252b44eac8ee726b3698009f3
+ $ python azure_vault.py --vault-name 'mydjangovault' --secret-name 'vaultpw' \
+ --secret-version 6b6w7f7252b44eac8ee726b3698009f3
+
+ Use with a playbook
+ $ ansible-playbook -i ./azure_rm.py my_playbook.yml --limit galaxy-qa --vault-password-file ./azure_vault.py
+
+
+Insecure Platform Warning
+-------------------------
+If you receive InsecurePlatformWarning from urllib3, install the
+requests security packages:
+
+ pip install requests[security]
+
+
+author:
+ - Chris Houseknecht (@chouseknecht)
+ - Matt Davis (@nitzmahone)
+ - Austin Hobbs (@OxHobbs)
+
+Company: Ansible by Red Hat, Microsoft
+
+Version: 0.1.0
+'''
+
+import argparse
+import os
+import re
+import sys
+import inspect
+from azure.keyvault import KeyVaultClient
+
+try:
+ # python2
+ import ConfigParser as cp
+except ImportError:
+ # python3
+ import configparser as cp
+
+from os.path import expanduser
+import ansible.module_utils.six.moves.urllib.parse as urlparse
+
+HAS_AZURE = True
+HAS_AZURE_EXC = None
+HAS_AZURE_CLI_CORE = True
+CLIError = None
+
+try:
+ from msrestazure.azure_active_directory import AADTokenCredentials
+ from msrestazure.azure_exceptions import CloudError
+ from msrestazure.azure_active_directory import MSIAuthentication
+ from msrestazure import azure_cloud
+ from azure.mgmt.compute import __version__ as azure_compute_version
+ from azure.common import AzureMissingResourceHttpError, AzureHttpError
+ from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials
+ from azure.mgmt.network import NetworkManagementClient
+ from azure.mgmt.resource.resources import ResourceManagementClient
+ from azure.mgmt.resource.subscriptions import SubscriptionClient
+ from azure.mgmt.compute import ComputeManagementClient
+ from adal.authentication_context import AuthenticationContext
+except ImportError as exc:
+ HAS_AZURE_EXC = exc
+ HAS_AZURE = False
+
+try:
+ from azure.cli.core.util import CLIError
+ from azure.common.credentials import get_azure_cli_credentials, get_cli_profile
+ from azure.common.cloud import get_cli_active_cloud
+except ImportError:
+ HAS_AZURE_CLI_CORE = False
+ CLIError = Exception
+
+try:
+ from ansible.release import __version__ as ansible_version
+except ImportError:
+ ansible_version = 'unknown'
+
+
+AZURE_CREDENTIAL_ENV_MAPPING = dict(
+ profile='AZURE_PROFILE',
+ subscription_id='AZURE_SUBSCRIPTION_ID',
+ client_id='AZURE_CLIENT_ID',
+ secret='AZURE_SECRET',
+ tenant='AZURE_TENANT',
+ ad_user='AZURE_AD_USER',
+ password='AZURE_PASSWORD',
+ cloud_environment='AZURE_CLOUD_ENVIRONMENT',
+ adfs_authority_url='AZURE_ADFS_AUTHORITY_URL'
+)
+
+AZURE_VAULT_SETTINGS = dict(
+ vault_name='AZURE_VAULT_NAME',
+ secret_name='AZURE_VAULT_SECRET_NAME',
+ secret_version='AZURE_VAULT_SECRET_VERSION',
+)
+
+AZURE_MIN_VERSION = "2.0.0"
+ANSIBLE_USER_AGENT = 'Ansible/{0}'.format(ansible_version)
+
+
+class AzureRM(object):
+
+ def __init__(self, args):
+ self._args = args
+ self._cloud_environment = None
+ self._compute_client = None
+ self._resource_client = None
+ self._network_client = None
+ self._adfs_authority_url = None
+ self._vault_client = None
+ self._resource = None
+
+ self.debug = False
+ if args.debug:
+ self.debug = True
+
+ self.credentials = self._get_credentials(args)
+ if not self.credentials:
+ self.fail("Failed to get credentials. Either pass as parameters, set environment variables, "
+ "or define a profile in ~/.azure/credentials.")
+
+ # if cloud_environment specified, look up/build Cloud object
+ raw_cloud_env = self.credentials.get('cloud_environment')
+ if not raw_cloud_env:
+ self._cloud_environment = azure_cloud.AZURE_PUBLIC_CLOUD # SDK default
+ else:
+ # try to look up "well-known" values via the name attribute on azure_cloud members
+ all_clouds = [x[1] for x in inspect.getmembers(azure_cloud) if isinstance(x[1], azure_cloud.Cloud)]
+ matched_clouds = [x for x in all_clouds if x.name == raw_cloud_env]
+ if len(matched_clouds) == 1:
+ self._cloud_environment = matched_clouds[0]
+ elif len(matched_clouds) > 1:
+ self.fail("Azure SDK failure: more than one cloud matched for cloud_environment name '{0}'".format(
+ raw_cloud_env))
+ else:
+ if not urlparse.urlparse(raw_cloud_env).scheme:
+ self.fail("cloud_environment must be an endpoint discovery URL or one of {0}".format(
+ [x.name for x in all_clouds]))
+ try:
+ self._cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(raw_cloud_env)
+ except Exception as e:
+ self.fail("cloud_environment {0} could not be resolved: {1}".format(raw_cloud_env, e.message))
+
+ if self.credentials.get('subscription_id', None) is None:
+ self.fail("Credentials did not include a subscription_id value.")
+ self.log("setting subscription_id")
+ self.subscription_id = self.credentials['subscription_id']
+
+ # get authentication authority
+ # for adfs, user could pass in authority or not.
+ # for others, use default authority from cloud environment
+ if self.credentials.get('adfs_authority_url'):
+ self._adfs_authority_url = self.credentials.get('adfs_authority_url')
+ else:
+ self._adfs_authority_url = self._cloud_environment.endpoints.active_directory
+
+ # get resource from cloud environment
+ self._resource = self._cloud_environment.endpoints.active_directory_resource_id
+
+ if self.credentials.get('credentials'):
+ self.azure_credentials = self.credentials.get('credentials')
+ elif self.credentials.get('client_id') and self.credentials.get('secret') and self.credentials.get('tenant'):
+ self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
+ secret=self.credentials['secret'],
+ tenant=self.credentials['tenant'],
+ cloud_environment=self._cloud_environment)
+
+ elif self.credentials.get('ad_user') is not None and \
+ self.credentials.get('password') is not None and \
+ self.credentials.get('client_id') is not None and \
+ self.credentials.get('tenant') is not None:
+
+ self.azure_credentials = self.acquire_token_with_username_password(
+ self._adfs_authority_url,
+ self._resource,
+ self.credentials['ad_user'],
+ self.credentials['password'],
+ self.credentials['client_id'],
+ self.credentials['tenant'])
+
+ elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None:
+ tenant = self.credentials.get('tenant')
+ if not tenant:
+ tenant = 'common'
+ self.azure_credentials = UserPassCredentials(self.credentials['ad_user'],
+ self.credentials['password'],
+ tenant=tenant,
+ cloud_environment=self._cloud_environment)
+
+ else:
+ self.fail("Failed to authenticate with provided credentials. Some attributes were missing. "
+ "Credentials must include client_id, secret and tenant or ad_user and password, or "
+ "ad_user, password, client_id, tenant and adfs_authority_url(optional) for ADFS authentication, "
+ "or be logged in using AzureCLI.")
+
+ def log(self, msg):
+ if self.debug:
+ print(msg + u'\n')
+
+ def fail(self, msg):
+ raise Exception(msg)
+
+ def _get_profile(self, profile="default"):
+ path = expanduser("~")
+ path += "/.azure/credentials"
+ try:
+ config = cp.ConfigParser()
+ config.read(path)
+ except Exception as exc:
+ self.fail("Failed to access {0}. Check that the file exists and you have read "
+ "access. {1}".format(path, str(exc)))
+ credentials = dict()
+ for key in AZURE_CREDENTIAL_ENV_MAPPING:
+ try:
+ credentials[key] = config.get(profile, key, raw=True)
+ except:
+ pass
+
+ if credentials.get('client_id') is not None or credentials.get('ad_user') is not None:
+ return credentials
+
+ return None
+
+ def _get_env_credentials(self):
+ env_credentials = dict()
+ for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
+ env_credentials[attribute] = os.environ.get(env_variable, None)
+
+ if env_credentials['profile'] is not None:
+ credentials = self._get_profile(env_credentials['profile'])
+ return credentials
+
+ if env_credentials['client_id'] is not None or env_credentials['ad_user'] is not None:
+ return env_credentials
+
+ return None
+
+ def _get_azure_cli_credentials(self):
+ credentials, subscription_id = get_azure_cli_credentials()
+ cloud_environment = get_cli_active_cloud()
+
+ cli_credentials = {
+ 'credentials': credentials,
+ 'subscription_id': subscription_id,
+ 'cloud_environment': cloud_environment
+ }
+ return cli_credentials
+
+ def _get_msi_credentials(self, subscription_id_param=None):
+ credentials = MSIAuthentication()
+ try:
+ # try to get the subscription in MSI to test whether MSI is enabled
+ subscription_client = SubscriptionClient(credentials)
+ subscription = next(subscription_client.subscriptions.list())
+ subscription_id = str(subscription.subscription_id)
+ return {
+ 'credentials': credentials,
+ 'subscription_id': subscription_id_param or subscription_id
+ }
+ except Exception as exc:
+ return None
+
+ def _get_credentials(self, params):
+ # Get authentication credentials.
+ # Precedence: cmd line parameters-> environment variables-> default profile in ~/.azure/credentials.
+
+ self.log('Getting credentials')
+
+ arg_credentials = dict()
+ for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
+ arg_credentials[attribute] = getattr(params, attribute)
+
+ # try module params
+ if arg_credentials['profile'] is not None:
+ self.log('Retrieving credentials with profile parameter.')
+ credentials = self._get_profile(arg_credentials['profile'])
+ return credentials
+
+ if arg_credentials['client_id'] is not None:
+ self.log('Received credentials from parameters.')
+ return arg_credentials
+
+ if arg_credentials['ad_user'] is not None:
+ self.log('Received credentials from parameters.')
+ return arg_credentials
+
+ # try environment
+ env_credentials = self._get_env_credentials()
+ if env_credentials:
+ self.log('Received credentials from env.')
+ return env_credentials
+
+ # try default profile from ~./azure/credentials
+ default_credentials = self._get_profile()
+ if default_credentials:
+ self.log('Retrieved default profile credentials from ~/.azure/credentials.')
+ return default_credentials
+
+ msi_credentials = self._get_msi_credentials(arg_credentials.get('subscription_id'))
+ if msi_credentials:
+ self.log('Retrieved credentials from MSI.')
+ return msi_credentials
+
+ try:
+ if HAS_AZURE_CLI_CORE:
+ self.log('Retrieving credentials from AzureCLI profile')
+ cli_credentials = self._get_azure_cli_credentials()
+ return cli_credentials
+ except CLIError as ce:
+ self.log('Error getting AzureCLI profile credentials - {0}'.format(ce))
+
+ return None
+
+ def acquire_token_with_username_password(self, authority, resource, username, password, client_id, tenant):
+ authority_uri = authority
+
+ if tenant is not None:
+ authority_uri = authority + '/' + tenant
+
+ context = AuthenticationContext(authority_uri)
+ token_response = context.acquire_token_with_username_password(resource, username, password, client_id)
+ return AADTokenCredentials(token_response)
+
+ def _register(self, key):
+ try:
+ # We have to perform the one-time registration here. Otherwise, we receive an error the first
+ # time we attempt to use the requested client.
+ resource_client = self.rm_client
+ resource_client.providers.register(key)
+ except Exception as exc:
+ self.log("One-time registration of {0} failed - {1}".format(key, str(exc)))
+ self.log("You might need to register {0} using an admin account".format(key))
+ self.log(("To register a provider using the Python CLI: "
+ "https://docs.microsoft.com/azure/azure-resource-manager/"
+ "resource-manager-common-deployment-errors#noregisteredproviderfound"))
+
+ def get_mgmt_svc_client(self, client_type, base_url, api_version):
+ client = client_type(self.azure_credentials,
+ self.subscription_id,
+ base_url=base_url,
+ api_version=api_version)
+ client.config.add_user_agent(ANSIBLE_USER_AGENT)
+ return client
+
+ def get_vault_client(self):
+ return KeyVaultClient(self.azure_credentials)
+
+ def get_vault_suffix(self):
+ return self._cloud_environment.suffixes.keyvault_dns
+
+ @property
+ def network_client(self):
+ self.log('Getting network client')
+ if not self._network_client:
+ self._network_client = self.get_mgmt_svc_client(NetworkManagementClient,
+ self._cloud_environment.endpoints.resource_manager,
+ '2017-06-01')
+ self._register('Microsoft.Network')
+ return self._network_client
+
+ @property
+ def rm_client(self):
+ self.log('Getting resource manager client')
+ if not self._resource_client:
+ self._resource_client = self.get_mgmt_svc_client(ResourceManagementClient,
+ self._cloud_environment.endpoints.resource_manager,
+ '2017-05-10')
+ return self._resource_client
+
+ @property
+ def compute_client(self):
+ self.log('Getting compute client')
+ if not self._compute_client:
+ self._compute_client = self.get_mgmt_svc_client(ComputeManagementClient,
+ self._cloud_environment.endpoints.resource_manager,
+ '2017-03-30')
+ self._register('Microsoft.Compute')
+ return self._compute_client
+
+ @property
+ def vault_client(self):
+ self.log('Getting the Key Vault client')
+ if not self._vault_client:
+ self._vault_client = self.get_vault_client()
+
+ return self._vault_client
+
+
+class AzureKeyVaultSecret:
+
+ def __init__(self):
+
+ self._args = self._parse_cli_args()
+
+ try:
+ rm = AzureRM(self._args)
+ except Exception as e:
+ sys.exit("{0}".format(str(e)))
+
+ self._get_vault_settings()
+
+ if self._args.vault_name:
+ self.vault_name = self._args.vault_name
+
+ if self._args.secret_name:
+ self.secret_name = self._args.secret_name
+
+ if self._args.secret_version:
+ self.secret_version = self._args.secret_version
+
+ self._vault_suffix = rm.get_vault_suffix()
+ self._vault_client = rm.vault_client
+
+ print(self.get_password_from_vault())
+
+ def _parse_cli_args(self):
+ parser = argparse.ArgumentParser(
+ description='Obtain the vault password used to secure your Ansilbe secrets'
+ )
+ parser.add_argument('-n', '--vault-name', action='store', help='Name of Azure Key Vault')
+ parser.add_argument('-s', '--secret-name', action='store',
+ help='Name of the secret stored in Azure Key Vault')
+ parser.add_argument('-v', '--secret-version', action='store',
+ help='Version of the secret to be retrieved')
+ parser.add_argument('--debug', action='store_true', default=False,
+ help='Send the debug messages to STDOUT')
+ parser.add_argument('--profile', action='store',
+ help='Azure profile contained in ~/.azure/credentials')
+ parser.add_argument('--subscription_id', action='store',
+ help='Azure Subscription Id')
+ parser.add_argument('--client_id', action='store',
+ help='Azure Client Id ')
+ parser.add_argument('--secret', action='store',
+ help='Azure Client Secret')
+ parser.add_argument('--tenant', action='store',
+ help='Azure Tenant Id')
+ parser.add_argument('--ad_user', action='store',
+ help='Active Directory User')
+ parser.add_argument('--password', action='store',
+ help='password')
+ parser.add_argument('--adfs_authority_url', action='store',
+ help='Azure ADFS authority url')
+ parser.add_argument('--cloud_environment', action='store',
+ help='Azure Cloud Environment name or metadata discovery URL')
+
+ return parser.parse_args()
+
+ def get_password_from_vault(self):
+ vault_url = 'https://{0}{1}'.format(self.vault_name, self._vault_suffix)
+ secret = self._vault_client.get_secret(vault_url, self.secret_name, self.secret_version)
+ return secret.value
+
+ def _get_vault_settings(self):
+ env_settings = self._get_vault_env_settings()
+ if None not in set(env_settings.values()):
+ for key in AZURE_VAULT_SETTINGS:
+ setattr(self, key, env_settings.get(key, None))
+ else:
+ file_settings = self._load_vault_settings()
+ if not file_settings:
+ return
+
+ for key in AZURE_VAULT_SETTINGS:
+ if file_settings.get(key):
+ setattr(self, key, file_settings.get(key))
+
+ def _get_vault_env_settings(self):
+ env_settings = dict()
+ for attribute, env_variable in AZURE_VAULT_SETTINGS.items():
+ env_settings[attribute] = os.environ.get(env_variable, None)
+ return env_settings
+
+ def _load_vault_settings(self):
+ basename = os.path.splitext(os.path.basename(__file__))[0]
+ default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini'))
+ path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_VAULT_INI_PATH', default_path)))
+ config = None
+ settings = None
+ try:
+ config = cp.ConfigParser()
+ config.read(path)
+ except:
+ pass
+
+ if config is not None:
+ settings = dict()
+ for key in AZURE_VAULT_SETTINGS:
+ try:
+ settings[key] = config.get('azure_keyvault', key, raw=True)
+ except:
+ pass
+
+ return settings
+
+
+def main():
+ if not HAS_AZURE:
+ sys.exit("The Azure python sdk is not installed (try `pip install 'azure>={0}' --upgrade`) - {1}".format(
+ AZURE_MIN_VERSION, HAS_AZURE_EXC))
+
+ AzureKeyVaultSecret()
+
+
+if __name__ == '__main__':
+ main()