From 14d9582960eafd4d98d0f4aac8a5ee0d6dcbf6d2 Mon Sep 17 00:00:00 2001 From: Bill Peck Date: Fri, 6 Sep 2024 11:58:57 -0400 Subject: [PATCH] WIP - Add HSM support to Key Vault --- meta/runtime.yml | 1 + plugins/module_utils/security_domain_utils.py | 61 ++++ plugins/modules/azure_rm_keyvault.py | 246 +++++++++++-- plugins/modules/azure_rm_keyvault_info.py | 113 +++++- .../azure_rm_keyvaultsecuritydomain.py | 330 ++++++++++++++++++ .../targets/azure_rm_keyvault/aliases | 1 + .../azure_rm_keyvault/files/cert_0.cer | 21 ++ .../azure_rm_keyvault/files/cert_0.key | 28 ++ .../azure_rm_keyvault/files/cert_1.cer | 21 ++ .../azure_rm_keyvault/files/cert_1.key | 28 ++ .../azure_rm_keyvault/files/cert_2.cer | 21 ++ .../azure_rm_keyvault/files/cert_2.key | 28 ++ .../targets/azure_rm_keyvault/tasks/main.yml | 253 ++++++++++++++ 13 files changed, 1110 insertions(+), 42 deletions(-) create mode 100644 plugins/module_utils/security_domain_utils.py create mode 100644 plugins/modules/azure_rm_keyvaultsecuritydomain.py create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_0.cer create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_0.key create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_1.cer create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_1.key create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_2.cer create mode 100644 tests/integration/targets/azure_rm_keyvault/files/cert_2.key diff --git a/meta/runtime.yml b/meta/runtime.yml index 7b8068455..8ea81b45d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -132,6 +132,7 @@ action_groups: - azure.azcollection.azure_rm_ipgroup_info - azure.azcollection.azure_rm_keyvault - azure.azcollection.azure_rm_keyvault_info + - azure.azcollection.azure_rm_keyvaultsecuritydomain - azure.azcollection.azure_rm_keyvaultkey - azure.azcollection.azure_rm_keyvaultkey_info - azure.azcollection.azure_rm_keyvaultsecret diff --git a/plugins/module_utils/security_domain_utils.py b/plugins/module_utils/security_domain_utils.py new file mode 100644 index 000000000..12b8239d2 --- /dev/null +++ b/plugins/module_utils/security_domain_utils.py @@ -0,0 +1,61 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +import array +import base64 +import hashlib +import secrets +import traceback + +try: + from cryptography.hazmat.primitives.serialization import Encoding + HAS_CRYPTO = True + HAS_CRYPTO_EXC = None +except ImportError: + Encoding = None + HAS_CRYPTO = False + HAS_CRYPTO_EXC = traceback.format_exc() + + +class Utils: + @staticmethod + def is_little_endian(): + a = array.array('H', [1]).tobytes() + # little endian: b'\x01\x00' + # big endian: b'\x00\x01' + return a[0] == 1 + + @staticmethod + def convert_to_uint16(b: bytearray): + ret = [0 for _ in range(len(b) // 2)] + for i in range(0, len(b), 2): + byte_order = 'little' if Utils.is_little_endian() else 'big' + ret[i // 2] = int.from_bytes(bytearray([b[i], b[i + 1]]), byteorder=byte_order, signed=False) + return ret + + @staticmethod + def get_random(cb): + ret = bytearray() + for _ in range(cb): + ret.append(secrets.randbits(8)) + return ret + + @staticmethod + def get_SHA256_thumbprint(cert): + public_bytes = cert.public_bytes(Encoding.DER) + return hashlib.sha256(public_bytes).digest() + + @staticmethod + def security_domain_b64_url_encode_for_x5c(s): + return base64.b64encode(s).decode('ascii') + + @staticmethod + def security_domain_b64_url_encode(s): + return base64.b64encode(s).decode('ascii').strip('=').replace('+', '-').replace('/', '_') + + +if __name__ == '__main__': + print(Utils.convert_to_uint16(bytearray([40, 30, 20, 10]))) diff --git a/plugins/modules/azure_rm_keyvault.py b/plugins/modules/azure_rm_keyvault.py index d5e128e52..a56108b64 100644 --- a/plugins/modules/azure_rm_keyvault.py +++ b/plugins/modules/azure_rm_keyvault.py @@ -25,8 +25,43 @@ vault_name: description: - Name of the vault. - required: True + - It is mutually exclusive with I(hsm_name) + required: False + type: str + hsm_name: + description: + - Name of the HSM. + - It is mutually exclusive with I(vault_name) + required: False type: str + administrators: + description: + - List of administrator OID's for data plane operations for Managed HSM. + - It is mutually exclusive with I(vault_name) + required: False + type: list + elements: str + default: [] + identity: + description: + - Identity for the HSM, not valid for vault_name. + - It is mutually exclusive with I(vault_name) + type: dict + version_added: '3.0.0' + suboptions: + type: + description: + - Type of the managed identity + choices: + - UserAssigned + - None + default: None + type: str + user_assigned_identity: + description: + - User Assigned Managed Identity associated to this resource + required: false + type: str location: description: - Resource location. If not set, location from the resource group will be used as default. @@ -35,6 +70,8 @@ description: - The Azure Active Directory tenant ID that should be used for authenticating requests to the key vault. type: str + aliases: + - tenant_id sku: description: - SKU details. @@ -57,6 +94,7 @@ description: - An array of 0 to 16 identities that have access to the key vault. - All identities in the array must use the same tenant ID as the key vault's tenant ID. + - It is mutually exclusive with I(hsm_name) type: list elements: dict suboptions: @@ -142,14 +180,17 @@ enabled_for_deployment: description: - Property to specify whether Azure Virtual Machines are permitted to retrieve certificates stored as secrets from the key vault. + - It is mutually exclusive with I(hsm_name) type: bool enabled_for_disk_encryption: description: - Property to specify whether Azure Disk Encryption is permitted to retrieve secrets from the vault and unwrap keys. + - It is mutually exclusive with I(hsm_name) type: bool enabled_for_template_deployment: description: - Property to specify whether Azure Resource Manager is permitted to retrieve secrets from the key vault. + - It is mutually exclusive with I(hsm_name) type: bool enable_soft_delete: description: @@ -215,12 +256,15 @@ ''' import time -from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt try: from azure.core.polling import LROPoller from azure.core.exceptions import ResourceNotFoundError from azure.mgmt.keyvault import KeyVaultManagementClient + from azure.mgmt.keyvault.models import (ManagedServiceIdentity, UserAssignedIdentity, NetworkRuleSet, + NetworkRuleBypassOptions, NetworkRuleAction, ManagedHsmProperties, + ManagedHsm, ManagedHsmSku) except ImportError: # This is handled in azure_rm_common pass @@ -230,7 +274,7 @@ class Actions: NoAction, Create, Update, Delete = range(4) -class AzureRMVaults(AzureRMModuleBase): +class AzureRMVaults(AzureRMModuleBaseExt): """Configuration class for an Azure RM Key Vault resource""" def __init__(self): @@ -240,18 +284,39 @@ def __init__(self): required=True ), vault_name=dict( - type='str', - required=True + type='str' + ), + hsm_name=dict( + type='str' ), location=dict( type='str' ), vault_tenant=dict( - type='str' + type='str', + aliases=['tenant_id'] ), sku=dict( type='dict' ), + administrators=dict( + type='list', + default=[], + elements='str' + ), + identity=dict( + type='dict', + options=dict( + type=dict( + type='str', + choices=['UserAssigned', 'None'], + default='None' + ), + user_assigned_identity=dict( + type="str", + ), + ), + ), access_policies=dict( type='list', elements='dict', @@ -316,19 +381,42 @@ def __init__(self): self.resource_group = None self.vault_name = None + self.hsm_name = None self.parameters = dict() self.tags = None + self.identity = None + self.administrators = None self.results = dict(changed=False) self.mgmt_client = None self.state = None self.to_do = Actions.NoAction + self._managed_identity = None + + required_one_of = [('vault_name', 'hsm_name')] + mutually_exclusive = [('vault_name', 'hsm_name'), + ('vault_name', 'identity'), + ('vault_name', 'administrators'), + ('hsm_name', 'enabled_for_deployment'), + ('hsm_name', 'enabled_for_disk_encryption'), + ('hsm_name', 'enabled_for_template_deployment'), + ('hsm_name', 'access_policies')] super(AzureRMVaults, self).__init__(derived_arg_spec=self.module_arg_spec, supports_check_mode=True, supports_tags=True, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, required_if=self.module_required_if) + @property + def managed_identity(self): + if not self._managed_identity: + self._managed_identity = {"identity": ManagedServiceIdentity, + "user_assigned": UserAssignedIdentity + } + return self._managed_identity + def exec_module(self, **kwargs): """Main module execution method""" @@ -341,8 +429,10 @@ def exec_module(self, **kwargs): self.parameters["location"] = kwargs[key] elif key == "vault_tenant": self.parameters.setdefault("properties", {})["tenant_id"] = kwargs[key] - elif key == "sku": + elif key == "sku" and self.vault_name is not None: self.parameters.setdefault("properties", {})["sku"] = kwargs[key] + elif key == "sku" and self.hsm_name is not None: + self.parameters["sku"] = kwargs[key] elif key == "access_policies": access_policies = kwargs[key] for policy in access_policies: @@ -389,10 +479,16 @@ def exec_module(self, **kwargs): if "location" not in self.parameters: self.parameters["location"] = resource_group.location - old_response = self.get_keyvault() + old_response = self.get_instance() + + curr_identity = old_response.get('identity') if old_response else None + update_identity, identity_result = self.update_single_managed_identity(curr_identity=curr_identity, + new_identity=self.identity) + if update_identity: + self.parameters["identity"] = identity_result if not old_response: - self.log("Key Vault instance doesn't exist") + self.log("Old instance doesn't exist") if self.state == 'absent': self.log("Old instance didn't exist") else: @@ -400,11 +496,11 @@ def exec_module(self, **kwargs): if not self.parameters['properties']['enable_purge_protection']: self.parameters['properties'].pop('enable_purge_protection') else: - self.log("Key Vault instance already exists") + self.log("Instance already exists") if self.state == 'absent': self.to_do = Actions.Delete elif self.state == 'present': - self.log("Need to check if Key Vault instance has to be deleted or may be updated") + self.log("Need to check if instance has to be deleted or may be updated") if not self.parameters['properties']['enable_purge_protection'] and \ ('enable_purge_protection' not in old_response['properties'] or not old_response['properties']['enable_purge_protection']): @@ -468,12 +564,19 @@ def exec_module(self, **kwargs): update_tags, newtags = self.update_tags(old_response.get('tags', dict())) + if self.hsm_name and \ + self.administrators != old_response["properties"]["initial_admin_object_ids"]: + self.to_do = Actions.Update + if update_tags: self.to_do = Actions.Update self.tags = newtags + if update_identity: + self.to_do = Actions.Update + if (self.to_do == Actions.Create) or (self.to_do == Actions.Update): - self.log("Need to Create / Update the Key Vault instance") + self.log("Need to Create / Update the instance") if self.check_mode: self.results['changed'] = True @@ -481,7 +584,13 @@ def exec_module(self, **kwargs): self.parameters["tags"] = self.tags - response = self.create_update_keyvault() + if self.vault_name is not None: + response = self.create_update_keyvault() + else: + response = self.create_update_hsm() + + if response is None: + response = self.get_instance() if not old_response: self.results['changed'] = True @@ -489,19 +598,22 @@ def exec_module(self, **kwargs): self.results['changed'] = old_response.__ne__(response) self.log("Creation / Update done") elif self.to_do == Actions.Delete: - self.log("Key Vault instance deleted") + self.log("Instance deleted") self.results['changed'] = True if self.check_mode: return self.results - self.delete_keyvault() + if self.vault_name is not None: + self.delete_keyvault() + else: + self.hsm_begin_delete() # make sure instance is actually deleted, for some Azure resources, instance is hanging around # for some time after deletion -- this should be really fixed in Azure - while self.get_keyvault(): + while self.get_instance(): time.sleep(20) else: - self.log("Key Vault instance unchanged") + self.log("Instance unchanged") self.results['changed'] = False response = old_response @@ -528,7 +640,46 @@ def create_update_keyvault(self): except Exception as exc: self.log('Error attempting to create the Key Vault instance.') self.fail("Error creating the Key Vault instance: {0}".format(str(exc))) - return response.as_dict() + return response and response.as_dict() or None + + def create_update_hsm(self): + ''' + Creates or updates HSM with the specified configuration. + + :return: deserialized HSM instance state dictionary + ''' + self.log("Creating / Updating the HSM instance {0}".format(self.hsm_name)) + + tenant_id = self.parameters.get('properties', {}).get('tenant_id') + enable_purge_protection = self.parameters.get('properties', {}).get('enable_purge_protection') + retention_days = self.parameters.get('properties', {}).get('soft_delete_retention_in_days') + administrators = self.administrators + bypass = None + default_action = None + public_network_access = None + properties = ManagedHsmProperties(tenant_id=tenant_id, + enable_purge_protection=enable_purge_protection, + soft_delete_retention_in_days=retention_days, + initial_admin_object_ids=administrators, + network_acls=_create_network_rule_set(bypass, default_action), + public_network_access=public_network_access) + sku = self.parameters.get('sku') + parameters = ManagedHsm(location=self.parameters.get('location'), + tags=self.parameters.get('tags'), + sku=ManagedHsmSku(name=sku.get('name'), family=sku.get('family')), + identity=self.parameters.get('identity'), + properties=properties) + try: + response = self.mgmt_client.managed_hsms.begin_create_or_update(resource_group_name=self.resource_group, + name=self.hsm_name, + parameters=parameters) + if isinstance(response, LROPoller): + response = self.get_poller_result(response) + + except Exception as exc: + self.log('Error attempting to create the HSM instance.') + self.fail("Error creating the HSM instance: {0}".format(str(exc))) + return response and response.as_dict() or None def delete_keyvault(self): ''' @@ -546,28 +697,63 @@ def delete_keyvault(self): return True - def get_keyvault(self): + def hsm_begin_delete(self): ''' - Gets the properties of the specified Key Vault. + Deletes specified hsm instance in the specified subscription and resource group. - :return: deserialized Key Vault instance state dictionary + :return: True ''' - self.log("Checking if the Key Vault instance {0} is present".format(self.vault_name)) - found = False + self.log("Deleting the hsm instance {0}".format(self.hsm_name)) try: - response = self.mgmt_client.vaults.get(resource_group_name=self.resource_group, - vault_name=self.vault_name) - found = True - self.log("Response : {0}".format(response)) - self.log("Key Vault instance : {0} found".format(response.name)) - except ResourceNotFoundError as e: - self.log('Did not find the Key Vault instance.') + response = self.mgmt_client.managed_hsms.begin_delete(resource_group_name=self.resource_group, + name=self.hsm_name) + except Exception as e: + self.log('Error attempting to delete the hsm instance.') + self.fail("Error deleting the hsm instance: {0}".format(str(e))) + + return True + + def get_instance(self): + ''' + Gets the properties of the specified Key Vault or HSM. + + :return: deserialized Key Vault or HSM instance state dictionary + ''' + found = False + + if self.vault_name is not None: + self.log("Checking if the Key Vault instance {0} is present".format(self.vault_name)) + try: + response = self.mgmt_client.vaults.get(resource_group_name=self.resource_group, + vault_name=self.vault_name) + found = True + self.log("Response : {0}".format(response)) + self.log("Key Vault instance : {0} found".format(response.name)) + except ResourceNotFoundError as e: + self.log('Did not find the Key Vault instance.') + + if self.hsm_name is not None: + self.log("Checking if the hsm instance {0} is present".format(self.hsm_name)) + try: + response = self.mgmt_client.managed_hsms.get(resource_group_name=self.resource_group, + name=self.hsm_name) + found = True + self.log("Response : {0}".format(response)) + self.log("HSM instance : {0} found".format(response.name)) + except ResourceNotFoundError as e: + self.log('Did not find the hsm instance.') + if found is True: return response.as_dict() return False +def _create_network_rule_set(bypass=None, default_action=None): + return NetworkRuleSet(bypass=bypass or NetworkRuleBypassOptions.azure_services.value, + default_action=default_action or NetworkRuleAction.allow.value) + + def main(): """Main execution""" AzureRMVaults() diff --git a/plugins/modules/azure_rm_keyvault_info.py b/plugins/modules/azure_rm_keyvault_info.py index 107f48e65..940e25730 100644 --- a/plugins/modules/azure_rm_keyvault_info.py +++ b/plugins/modules/azure_rm_keyvault_info.py @@ -25,6 +25,9 @@ description: - The name of the key vault. type: str + hsm_name: + description: + - The name of the HSM. tags: description: - Limit results by providing a list of tags. Format tags as 'key' or 'key:value'. @@ -231,17 +234,39 @@ def keyvault_to_dict(vault): ) +def hsm_to_dict(hsm): + return dict( + id=hsm.id, + name=hsm.name, + hsm_uri=hsm.properties.hsm_uri, + location=hsm.location, + tags=hsm.tags, + identity=hsm.identity and hsm.identity.as_dict() or None, + enable_soft_delete=hsm.properties.enable_soft_delete, + soft_delete_retention_in_days=hsm.properties.soft_delete_retention_in_days + if hsm.properties.soft_delete_retention_in_days else 90, + enable_purge_protection=hsm.properties.enable_purge_protection + if hsm.properties.enable_purge_protection else False, + sku=dict( + family=hsm.sku.family, + name=hsm.sku.name + ) + ) + + class AzureRMKeyVaultInfo(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( resource_group=dict(type='str'), name=dict(type='str'), + hsm_name=dict(type='str'), tags=dict(type='list', elements='str') ) self.resource_group = None self.name = None + self.hsm_name = None self.tags = None self.results = dict(changed=False) @@ -265,37 +290,79 @@ def exec_module(self, **kwargs): if self.name: if self.resource_group: - self.results['keyvaults'] = self.get_by_name() + self.results['keyvaults'] = self.get_by_vault_name() else: self.fail("resource_group is required when filtering by name") + elif self.hsm_name: + if self.resource_group: + self.results['hsms'] = self.get_by_hsm_name() + else: + self.fail("resource_group is required when filtering by hsm_name") elif self.resource_group: - self.results['keyvaults'] = self.list_by_resource_group() + self.results['keyvaults'] = self.list_vault_by_resource_group() + self.results['hsms'] = self.list_hsm_by_resource_group_hsm() else: - self.results['keyvaults'] = self.list() + self.results['keyvaults'] = self.list_vault() + self.results['hsms'] = self.list_hsm() return self.results - def get_by_name(self): + def get_by_hsm_name(self): ''' - Gets the properties of the specified key vault. + Gets the properties of this specified hsm. - :return: deserialized key vaultstate dictionary + :return: deserialized hsm state dictionary ''' - self.log("Get the key vault {0}".format(self.name)) - + self.log("Get the hsm {0}".format(self.hsm_name)) results = [] try: - response = self._client.vaults.get(resource_group_name=self.resource_group, - vault_name=self.name) + response = self._client.managed_hsms.get(resource_group_name=self.resource_group, name=self.hsm_name) self.log("Response : {0}".format(response)) + if response and self.has_tags(response.tags, self.tags): + results.append(hsm_to_dict(response)) + except ResourceNotFoundError as e: + self.log("Did not find the hsm {0}: {1}".format(self.hsm_name, str(e))) + return results + + def get_by_vault_name(self): + ''' + Gets the properties of this specified key vault. + :return: deserialized key vault state dictionary + ''' + self.log("Get the key vault {0}".format(self.name)) + results = [] + try: + response = self._client.vaults.get(resource_group_name=self.resource_group, vault_name=self.name) + self.log("Response : {0}".format(response)) if response and self.has_tags(response.tags, self.tags): results.append(keyvault_to_dict(response)) except ResourceNotFoundError as e: self.log("Did not find the key vault {0}: {1}".format(self.name, str(e))) return results - def list_by_resource_group(self): + def list_hsm_by_resource_group(self): + ''' + Lists the properties of hsms in specific resource group. + + :return: deserialized hsm state dictionary + ''' + self.log("Get the hsms in resource group {0}".format(self.resource_group)) + + results = [] + try: + response = list(self._client.managed_hsms.list_by_resource_group(resource_group_name=self.resource_group)) + self.log("Response : {0}".format(response)) + + if response: + for item in response: + if self.has_tags(item.tags, self.tags): + results.append(hsm_to_dict(item)) + except Exception as e: + self.log("Did not find hsms in resource group {0} : {1}.".format(self.resource_group, str(e))) + return results + + def list_vault_by_resource_group(self): ''' Lists the properties of key vaults in specific resource group. @@ -316,7 +383,7 @@ def list_by_resource_group(self): self.log("Did not find key vaults in resource group {0} : {1}.".format(self.resource_group, str(e))) return results - def list(self): + def list_vault(self): ''' Lists the properties of key vaults in specific subscription. @@ -338,6 +405,28 @@ def list(self): self.log("Did not find key vault in current subscription {0}.".format(str(e))) return results + def list_hsm(self): + ''' + Lists the properties of hsms in specific subscription. + + :return: deserialized hsms state dictionary + ''' + self.log("Get the hsms in current subscription") + + results = [] + try: + response = list(self._client.managed_hsms.list_by_subscription()) + self.log("Response : {0}".format(response)) + + if response: + for item in response: + if self.has_tags(item.tags, self.tags): + source_id = item.id.split('/') + results.append(hsm_to_dict(self._client.managed_hsms.get(source_id[4], source_id[8]))) + except Exception as e: + self.log("Did not find hsm in current subscription {0}.".format(str(e))) + return results + def main(): """Main execution""" diff --git a/plugins/modules/azure_rm_keyvaultsecuritydomain.py b/plugins/modules/azure_rm_keyvaultsecuritydomain.py new file mode 100644 index 000000000..9f09e6cba --- /dev/null +++ b/plugins/modules/azure_rm_keyvaultsecuritydomain.py @@ -0,0 +1,330 @@ +#!/usr/bin/python +# +# Copyright (c) 2024 Bill Peck, +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: azure_rm_keyvaultsecuritydomain +version_added: "3.0.0" +short_description: Manage Key Vault security domain for HSM +description: + - Create and delete instance of Key Vault Security Domain. + +options: + keyvault_uri: + description: + - URI of the keyvault endpoint. + type: str + hsm_name: + description: + - Name of the HSM. + type: str + sd_quorum: + description: + - The minimum number of shares required to decrypt the security domain for recovery. + - The quorum of security domain should be in range [2, 10]. + required: True + type: int + sd_wrapping_keys: + description: + - List of wrapping keys containing public keys. + - The number of wrapping keys should be in range [3, 10]. + required: True + type: list + elements: str + no_wait: + description: + - Don't wait for the operation to finish + type: bool + default: False + action: + description: + - Action to take on Security Domain. Use C(download) to download security domain file and C(upload) to restore the HSM. + default: download + type: str + choices: + - download + - upload + +extends_documentation_fragment: + - azure.azcollection.azure + +author: + - Bill Peck (@p3ck) + +''' + +EXAMPLES = ''' +- name: Download Security domain file + azure_rm_keyvaultsecuritydomain: + keyvault_uri: https://samplehsmvault.managedhsm.azure.net + sd_quorum: 2 + sd_wrapping_keys: + - "{{ lookup('file', 'certfile1') }}" + - "{{ lookup('file', 'certfile2') }}" + - "{{ lookup('file', 'certfile3') }}" + action: download + +- name: Upload Security domain + azure_rm_keyvaultsecuritydomain: + keyvault_uri: https://samplehsmvault.managedhsm.azure.net + action: upload +''' + +RETURN = ''' +security_domain: + description: + - JSON blob + returned: always + type: str +''' + +import time +import codecs +import hashlib +import traceback +from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common_ext import AzureRMModuleBaseExt +from ansible.module_utils.basic import missing_required_lib +from ansible_collections.azure.azcollection.plugins.module_utils.security_domain_utils import Utils + + +try: + from cryptography.x509 import load_pem_x509_certificate + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding + from cryptography.hazmat.primitives.asymmetric import rsa + HAS_CRYPTO = True + HAS_CRYPTO_EXC = None +except ImportError: + load_pem_x509_certificate = None + default_backend = None + Encoding = None + rsa = None + HAS_CRYPTO = False + HAS_CRYPTO_EXC = traceback.format_exc() + + +try: + from azure.cli.command_modules.keyvault.vendored_sdks.azure_keyvault_t1.v7_2.models import CertificateSet, SecurityDomainJsonWebKey + from azure.cli.command_modules.keyvault.vendored_sdks.azure_keyvault_t1 import KeyVaultClient, KeyVaultAuthentication + HAS_AZURE_CLI = True + HAS_AZURE_CLI_EXC = None +except ImportError: + CertificateSet = None + SecurityDomainJsonWebKey = None + KeyVaultClient = None + KeyVaultAuthentication = None + HAS_AZURE_CLI = False + HAS_AZURE_CLI_EXC = traceback.format_exc() + + +class AzureRMVaultSecurityDomain(AzureRMModuleBaseExt): + """Configuration class for an Azure RM Vault Security Domain resource""" + + def __init__(self): + self.module_arg_spec = dict( + keyvault_uri=dict( + no_log=True, + type='str' + ), + hsm_name=dict( + type='str' + ), + sd_wrapping_keys=dict( + type='list', + required=True, + no_log=True, + elements='str' + ), + sd_quorum=dict( + type='int', + required=True + ), + no_wait=dict( + type='bool', + default='False' + ), + action=dict( + type='str', + default='download', + choices=['download', 'upload'] + ) + ) + + self.module_required_if = [] + + self.keyvault_uri = None + self.hsm_name = None + + self.results = dict(changed=False) + self.client = None + self.action = None + self.sd_wrapping_keys = [] + self.sd_quorum = None + self.no_wait = False + + required_one_of = [('keyvault_uri', 'hsm_name')] + + super(AzureRMVaultSecurityDomain, self).__init__(derived_arg_spec=self.module_arg_spec, + supports_check_mode=False, + supports_tags=False, + required_one_of=required_one_of, + required_if=self.module_required_if) + + if not HAS_CRYPTO: + self.fail(msg=missing_required_lib('cryptography'), + exception=HAS_CRYPTO_EXC) + + if not HAS_AZURE_CLI: + self.fail(msg=missing_required_lib('azure-cli'), + exception=HAS_AZURE_CLI_EXC) + + def exec_module(self, **kwargs): + """Main module execution method""" + + # translate Ansible input to SDK-formatted dict in self.parameters + for key in list(self.module_arg_spec.keys()): + if hasattr(self, key): + setattr(self, key, kwargs[key]) + + self.client = self.get_keyvault_client() + + response = None + if self.action == 'download': + response = self.security_domain_download() + + if response: + self.results["security_domain"] = response.value + + return self.results + + def get_keyvault_client(self): + + def auth_callack(server, resource, scope): + from collections import namedtuple + AccessToken = namedtuple('AccessToken', ['scheme', 'token', 'key']) + AccessToken.__new__.__defaults__ = ('Bearer', None, None) + + if scope is None or scope == '': + base_url = resource + if not base_url.endswith("/"): + base_url += "/" + scope = [base_url + ".default"] + + token = self.azure_auth.azure_credential_track2.get_token(*scope) + + return AccessToken(token=token.token) + + return KeyVaultClient(KeyVaultAuthentication(auth_callack), api_version='7.2') + + def security_domain_download(self): + + N = len(self.sd_wrapping_keys) + if N < 3 or N > 10: + self.fail('The number of wrapping keys {0} should be in range [3, 10].'.format(N)) + if self.sd_quorum < 2 or self.sd_quorum > 10: + self.fail('The quorum of security domain {0} should be in range [2, 10].'.format(self.sd_quorum)) + + certificates = [] + for pem_string in self.sd_wrapping_keys: + pem_data = pem_string.encode('UTF-8') + + sd_jwk = SecurityDomainJsonWebKey() + + cert = load_pem_x509_certificate(pem_data, backend=default_backend()) + public_key = cert.public_key() + public_bytes = cert.public_bytes(Encoding.DER) + sd_jwk.x5c = [Utils.security_domain_b64_url_encode_for_x5c(public_bytes)] # only one cert, not a chain + sd_jwk.x5t = Utils.security_domain_b64_url_encode(hashlib.sha1(public_bytes).digest()) + sd_jwk.x5tS256 = Utils.security_domain_b64_url_encode(hashlib.sha256(public_bytes).digest()) + sd_jwk.key_ops = ['verify', 'encrypt', 'wrapKey'] + + # populate key into jwk + if isinstance(public_key, rsa.RSAPublicKey) and public_key.key_size >= 2048: + sd_jwk.kty = 'RSA' + sd_jwk.alg = 'RSA-OAEP-256' + _public_rsa_key_to_jwk(public_key, sd_jwk, encoding=Utils.security_domain_b64_url_encode) + else: + self.fail('Only RSA >= 2048 is supported.') + + certificates.append(sd_jwk) + + ret = self.client.download( + vault_base_url=self.hsm_name or self.keyvault_uri, + certificates=CertificateSet(certificates=certificates, required=self.sd_quorum) + ) + + if not self.no_wait: + wait_second = 5 + time.sleep(wait_second) + polling_ret = _wait_security_domain_operation(self.client, + self.hsm_name, + 'download', + vault_base_url=self.keyvault_uri) + if polling_ret is None or getattr(polling_ret, 'status', None) == 'Failed': + return polling_ret + + return ret + + +def _int_to_bytes(i): + h = hex(i) + if len(h) > 1 and h[0:2] == '0x': + h = h[2:] + # need to strip L in python 2.x + h = h.strip('L') + if len(h) % 2: + h = '0' + h + return codecs.decode(h, 'hex') + + +def _public_rsa_key_to_jwk(rsa_key, jwk, encoding=None): + pubv = rsa_key.public_numbers() + jwk.n = _int_to_bytes(pubv.n) + if encoding: + jwk.n = encoding(jwk.n) + jwk.e = _int_to_bytes(pubv.e) + if encoding: + jwk.e = encoding(jwk.e) + + +def _wait_security_domain_operation(client, + hsm_name, + target_operation='upload', + vault_base_url=None): + retries = 0 + max_retries = 30 + wait_second = 5 + while retries < max_retries: + try: + ret = None + if target_operation == 'upload': + ret = client.upload_pending(vault_base_url=hsm_name or vault_base_url) + elif target_operation == 'download': + ret = client.download_pending(vault_base_url=hsm_name or vault_base_url) + + # v7.2-preview and v7.2 will change the upload operation from Sync to Async + # due to service defects, it returns 'Succeeded' before the change and 'Success' after the change + if ret and getattr(ret, 'status', None) in ['Succeeded', 'Success', 'Failed']: + return ret + except Exception: + pass + time.sleep(wait_second) + retries += 1 + + return None + + +def main(): + """Main execution""" + AzureRMVaultSecurityDomain() + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/azure_rm_keyvault/aliases b/tests/integration/targets/azure_rm_keyvault/aliases index c256751e5..abfa5c4fa 100644 --- a/tests/integration/targets/azure_rm_keyvault/aliases +++ b/tests/integration/targets/azure_rm_keyvault/aliases @@ -3,3 +3,4 @@ destructive shippable/azure/group9 azure_rm_keyvaultkey azure_rm_keyvaultsecret +needs/file/tests/integration_common_tasks/managed_identity.yml diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_0.cer b/tests/integration/targets/azure_rm_keyvault/files/cert_0.cer new file mode 100644 index 000000000..d30a0e468 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_0.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgIUXfYLSWYXKP7p3N3WEjJg9azWafIwDQYJKoZIhvcNAQEL +BQAwSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYDVQQHDAdkb3VnbGFz +MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMB4XDTI0MDkwOTE5MDcyNloX +DTI1MDkwOTE5MDcyNlowSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYD +VQQHDAdkb3VnbGFzMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkr7wVfL+iNsHto6rM1fwQvWQcyVO +lIkYCZKGpTunR6W4utdvNSHKtsp6LKxc08D+RuGiAdZhjn4h2NdyVOaOCfAHIEUn +l4rywUx/umJukGVpS7sXkdYhlhbGvDEe67rEIE31BUXbhPCEiVeOIgrgwCYr2e2s +19jWu9KnkJPI8ySxRNskrmWu3uYpPfLcFpg6Z6XOdYu0Qq9PLDzceWq8TTLKXHYO +I1CZicbLemOvRFGVNj2OsQ+R9+O3WMmE5fCglxsqSInc6srb2Otu7mUjkPEBmqY1 +kzjeOFSvUyS9O/1oSW97HYf9Jmsr8YvqwFKHfeR5xCXUXxuBa5Mbj0t4RQIDAQAB +o1MwUTAdBgNVHQ4EFgQUHTJN2UVf4iLikJXgkRLxL38Hi/UwHwYDVR0jBBgwFoAU +HTJN2UVf4iLikJXgkRLxL38Hi/UwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEAYwWNX0qGIuLLu4YuWZrwQg/XErcudTUjzCPb9mwWVjjphXlPMiaW +XOX4jalAFPz00cccfL1YNv21RTdOxwrvBifgGIe8EMv+vFhLKJKm64jSAmPX1ttw +O/t1sNhXU2Rp6Y9vF0nV6lrVXv0YGIUEGedcMxuVdYq8qbmqVqejqKWCJyHHInIR +Pi8feX6QY2fQI7KwfxmEvquWvXy7BGy+zaILe+CJcUFySCl/WHd/stUzxPBdIBne +u3Iv1hX8PmTpYFcnfJqty03ZxAflMlQO3ZzsVqmzgBUcUrotCi7j9esRjtOSI3nW +sxnoYqfxNPqYWMqmkJUpR5YxuUSXJbGj4Q== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_0.key b/tests/integration/targets/azure_rm_keyvault/files/cert_0.key new file mode 100644 index 000000000..2baec7029 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_0.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSvvBV8v6I2we2 +jqszV/BC9ZBzJU6UiRgJkoalO6dHpbi61281Icq2ynosrFzTwP5G4aIB1mGOfiHY +13JU5o4J8AcgRSeXivLBTH+6Ym6QZWlLuxeR1iGWFsa8MR7rusQgTfUFRduE8ISJ +V44iCuDAJivZ7azX2Na70qeQk8jzJLFE2ySuZa7e5ik98twWmDpnpc51i7RCr08s +PNx5arxNMspcdg4jUJmJxst6Y69EUZU2PY6xD5H347dYyYTl8KCXGypIidzqytvY +627uZSOQ8QGapjWTON44VK9TJL07/WhJb3sdh/0mayvxi+rAUod95HnEJdRfG4Fr +kxuPS3hFAgMBAAECggEAfPRjd/xy6xv+5F91vkGKT6oEd1f0IpzeQABp8LbsCSE/ +PLIHeumsUJv3DqUyYgl7O+YTapce+RPERH2oWEz9885UcxEP1oW1kg1O0enRFdmU +oKzONBtu+/um/EajerzNFmjrU7MZaojXgo9wcuJqYJPgUTCGNkHpD4QftQdyXD6/ +MWkMYqDeldE1mX/QK7sOGyStGRSyMf6lL0mKIUXObNaPTZS1B9Pto85LK5iJkiIw +WuDLAoKd2x5Kj+SN3T8XNTmaJJJ6NZ+k3AubKGy4JEDma+LPfLo+70Ewp6KIVKkR +utf0UqIDWngDbRHG089XRGgKrQ6WUNmh/skVZMwCPQKBgQDCizNXeI39TkKlZRzP +7VKvLSsdEVCz9F9Kr7bAcN16x3EXPxXJvTSJ2g26NJZjfsv79ZaAsyevWHPQmP5t +FwoVnx3YqUJC+Cw0egkuwycOXV3B+CP4u8O9FKF0lnndNstUJSC38NJkQHbNXEQX +RUwN419x2rLpsuS2KYwk5byZBwKBgQDBGk+ZvbvoH9llkSDoADVyicJ/mdlWtgem +glsQYdwQd2hFrn5vi6cx7r61yDITa+KXnCXKlUhaIDf1nuav9aHYKy04A7aj89Ap +1U0Fgnj3/S/PFqRxt9jTDhMUPFfckdbGGMRn090UwrCEhnB8Fn3ZGcguUTNywYlZ +fjV/T/uNUwKBgDQnHwdnASGT9lfiiFvRcmYVxMYRG0Jy04zxGBv05dsBVnb16YBg +oZIHC8EMUfiwSDzudH9iB9SA8ONN8H8MOx7aviSUE3hikW8r/AQ2OuUl8HmMbRBE +PdAVlMbthBPimZWgMmo9PBm2EmMxReu7Hw1mE/Mwvt/ZnmibML+/etTHAoGBAJr8 +vDEAaSZJEdsEXe335O4WhcaWvCttlLxfWinO8atBu65Z/F8ZLsvT/Lu4gAC4kbjv ++iEcKmM0AtYggLVwKENxfCy+RkRXd5dr/RLUArXAQDQtzzT6w4u6ezO9ryN45nI/ +BLz0/jggfz8PDI98Gew7VkFeqTWNAumSc+vITXXDAoGAKczlMGdElMZreDFHzP2t +SEt7II19vUNP0OOun47XHrhjcGWs0xUzni9O3sqyiB/RwD0QhvHuhnvLxzsyC0/I +JjHRRmx/IuM7g+IpRpHo0Lgf5m1KFPIsTAAYvhygfBFriX5yQGOsXpJndCXwn0wL +DuZpvdqVscthDh5tfi3keCg= +-----END PRIVATE KEY----- diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_1.cer b/tests/integration/targets/azure_rm_keyvault/files/cert_1.cer new file mode 100644 index 000000000..69a9f8225 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_1.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgIUd/HvLMw93w1Xl6FmNWHSmOpTsAcwDQYJKoZIhvcNAQEL +BQAwSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYDVQQHDAdkb3VnbGFz +MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMB4XDTI0MDkxMDEzNTIyMVoX +DTI1MDkxMDEzNTIyMVowSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYD +VQQHDAdkb3VnbGFzMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtUOplvszwuvJuANglla1zq31mkRH +gBHHCnMiQJzL4ZSrFVa79lnIrR4XpKkh406FhAWbM3qTJOJazoV6U6uEWxvrULwH +ZDJ1IMtPNXppGmw/O8V1QOU8rSHBElXqEhJeOhLd0sBlm/3KvqtXY45rofAFGpjn +CwgvqkuuuV1KcRCporkYpuWxWVPORzu3uRYNTicE4u42GpCvsopYf6McX8LTDvNI +P9RET2yFf1ebk7RL8XHNCJ/CLAYdyhIuSeR0iOqDxTT4voihbMn/vV8GPMDF/xM0 +ztTJs2jg+vw7/H4NzWKKs0DddI9GiSFa8W5OmlzFITXhjphAQuprMB40/QIDAQAB +o1MwUTAdBgNVHQ4EFgQUXi9vAZOGkd13jqG+xC/YY3sBCSUwHwYDVR0jBBgwFoAU +Xi9vAZOGkd13jqG+xC/YY3sBCSUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEACKsLYZlJqef9Zas5+DaUpdZ9Up4rJ3jCXLVvWy0QmQ7jfwFTP3Sl +bvXEXyCMzXi+LZ9M6qoETd0A+qXXgYxuELwYey/crmSiV4LKTqqncAuspcKa/PU1 +KNe5lJxVht05/ivfrMoO76aJRlHXfZR7ydL2n1S9vsNkjtnN0848qM/2ndA7nQi3 +4X6VIPjUmk4zWh5FAcnv6J4rmNNXHJNJYNdOAsVP7wgJpLuQu7DGvWhe+0keWO4x +ibq+IYLnPe43YUdJHxYJKWc14uuldxBXD3LwU0REJNVK+YqoF6B6D7IW6mYnbY9k +B9livGFw0YNNMaAGgcOLxWVVKg3D3zmhjw== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_1.key b/tests/integration/targets/azure_rm_keyvault/files/cert_1.key new file mode 100644 index 000000000..60070b1d3 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_1.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1Q6mW+zPC68m4 +A2CWVrXOrfWaREeAEccKcyJAnMvhlKsVVrv2WcitHhekqSHjToWEBZszepMk4lrO +hXpTq4RbG+tQvAdkMnUgy081emkabD87xXVA5TytIcESVeoSEl46Et3SwGWb/cq+ +q1djjmuh8AUamOcLCC+qS665XUpxEKmiuRim5bFZU85HO7e5Fg1OJwTi7jYakK+y +ilh/oxxfwtMO80g/1ERPbIV/V5uTtEvxcc0In8IsBh3KEi5J5HSI6oPFNPi+iKFs +yf+9XwY8wMX/EzTO1MmzaOD6/Dv8fg3NYoqzQN10j0aJIVrxbk6aXMUhNeGOmEBC +6mswHjT9AgMBAAECggEAfOerLQbcnCyuS8bH/9CwZ0MoQq1aN74IUgMUT0G8nC09 +1u51h0RHLEPYNvb1CxVIm7jhQY/tZTU1Lap8qLs/8ShD9tYaocjDPV3brxYy5qpA +yIdATP+p2AOyb1gUe298zrfBc0BwxBUWaFzZUxkIwgYK/lDupIN3lPmh5MmMSmvu +42tTqLZ5chqooPgtouaKSnFmVkkvSeEw3vtZ4JJijyZzZzUcS79Ax6tMSjdmxw26 +VmpLMM0qpS9YGZV+Pf1/haxZI7yiN9RzpLJxYqOLDIxrCreiZdMuRJTys84L1qra +btYdbcEPdfgjfAuxaUy5X0I+O/olw5Fzck1G/67pAQKBgQDrtiBBjXpX1xpjo/uS +pp9I6mPEsXEO0uaF0Wh4Juis656xuwMagK+/Tvo4L2ru/PFFX6HH7idV9XhFzgB4 +yG2xmWG8DCJo1nnymeGKJbZ1aZBGjSdezIQWl+TehPazpc/93YXT0occayxgND5v +jxqfCJBvJ1p85HDp1iLdr7CCFQKBgQDE3czoN/kZ4SnkMztYez5pxmxPhfp4B9rO +87Or2sR8O8+VZDdEdkpoB39581gxbWwsO1l3XhXEZ79V9hi9PaUITZFnQ2b4Rsz/ +IchFlVzvbJxuzncTkC7vpqF9t7q1EnssaDe1FlZ0IPh0kgXO1KobhmUWoeBH+J0G +rbdmm9TpSQKBgQDWyUwVT91HA9ypJTlOBgUphWRKTMLQFkA3en2u1w243K/sFpSa +Zt8+/bGm1xajFdypMZ6TN6Gig58IRNJLPaAvcKwNliUY0S+ocK6Dmx/rV7k/gMp6 +aPSIPfsxBYpkY1jnZR/YyIOT0tlKBPFL6OQCPOSYVQzwt51oh1eYGMtHjQKBgGzp +jHA4by57HDLsiPuFi1z3cnp4U75OEiaGOrNr32IfsNMkU1Mj4jw1UbgFAZiuwbai +yvc37PDwuLD06nDfhsrWJwgrCO94M/c+GE8ut/CZdN30iXogPWdGF3e2yqtcYxqJ +ObCMgB3VE79h/aaUjtuVeZ2QxsTqbO0B9EHnGl5JAoGBAKjOAa6CvR1IGGAvHKII +7EZSLRxHHLeQShxvO0xgruZJJVIhJtz+RuocOvvDifkENR0vVAvz60iBfKs1lTYQ +xrX6Wp3Y8IXsB4j5VefT3/71J3PRTUpyrh7y3sELoH0OCotjCXNXff7Gy4h00aE9 +8tCuxKtkZfu/b5yw+j/X4qxi +-----END PRIVATE KEY----- diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_2.cer b/tests/integration/targets/azure_rm_keyvault/files/cert_2.cer new file mode 100644 index 000000000..809da0e54 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_2.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgIUWJh8bOVrObwDgaBX9yOwIJ1RYOEwDQYJKoZIhvcNAQEL +BQAwSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYDVQQHDAdkb3VnbGFz +MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMB4XDTI0MDkxMDEzNTI0MVoX +DTI1MDkxMDEzNTI0MVowSjELMAkGA1UEBhMCdXMxCzAJBgNVBAgMAm1hMRAwDgYD +VQQHDAdkb3VnbGFzMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsSMvl/O1PA3x+DOx6gM5Gt/Rb+ew +61vjmDy8X7kFbNawIUwYaLcclpAVstxzOqNIDBgA2E81MIV7x9EFEh7cCpB+7lpo +Lpg5uL5qTBLxwifvoHQEIkOOT9NSLiD6rfiLhQ0qvZXCwVbQ0RboaCyASuGZDnIr +cjNM3WokBifVnh4sAR7E4tKyXWMnxDIaViVY21x3pqg4dJI1WoGYK9WHt3UQnaTm +TVHwif+htLRxzULFMJDT5tMy9R5qi6RbRs7WOrne09RyQcPEesOUJrwflKqAATYD +Y6CErmb+E40GVpOqs7EQ9casbcsxMPva4ts4Oindioo4ftp+LKmcokXlCQIDAQAB +o1MwUTAdBgNVHQ4EFgQUM3YqIDMWwrkWXx3occ11SXRmvNwwHwYDVR0jBBgwFoAU +M3YqIDMWwrkWXx3occ11SXRmvNwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEAL69qDyPjnIhR/QAGks3G+/rlX4niaLdwuBaK7UZ2pZHCLlfQ+K3I +VIZnAJmYN0N4m21J2QceQFqnKUFNnGSZhMI0ajWANK7cygjwYvW1tlYj9SUs9Mvv +529VbjYcpFCrxRJa5iLfn8zaPhI/27DVKnXXK0Ya6ksWqPYrIlkLWdFQcQCMXVby +H9L8wl9qFiHE7+xXNxSoSkkddi1Zky+pdlFbOyPigxIFSceYSylAKM2VzGpdHkn8 +DiP0AIeA4E3G2cBWibfe/QGXOuRE2xi+oyDwSONLd0eBIvSm+Edc3gxraFvlklZC +49A8uHafm8ra3mS9lvYpTIpMw/fF6xnsbw== +-----END CERTIFICATE----- diff --git a/tests/integration/targets/azure_rm_keyvault/files/cert_2.key b/tests/integration/targets/azure_rm_keyvault/files/cert_2.key new file mode 100644 index 000000000..b74b7e425 --- /dev/null +++ b/tests/integration/targets/azure_rm_keyvault/files/cert_2.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxIy+X87U8DfH4 +M7HqAzka39Fv57DrW+OYPLxfuQVs1rAhTBhotxyWkBWy3HM6o0gMGADYTzUwhXvH +0QUSHtwKkH7uWmgumDm4vmpMEvHCJ++gdAQiQ45P01IuIPqt+IuFDSq9lcLBVtDR +FuhoLIBK4ZkOcityM0zdaiQGJ9WeHiwBHsTi0rJdYyfEMhpWJVjbXHemqDh0kjVa +gZgr1Ye3dRCdpOZNUfCJ/6G0tHHNQsUwkNPm0zL1HmqLpFtGztY6ud7T1HJBw8R6 +w5QmvB+UqoABNgNjoISuZv4TjQZWk6qzsRD1xqxtyzEw+9ri2zg6Kd2Kijh+2n4s +qZyiReUJAgMBAAECggEAWGRFeJ7NHjWbPA6Xxj1rfoSXXy8PcrK8xJfyLBYIpgIP +i94MBBzzsBIgMcud2HHoHtjPeyEzWo4RcWlVDXDGvqLSJCCLAk1F6HFkW6fMaeVB +xyB9e5eYaS77QDeGv89Z17/1Rvt3XsDynJPAz1L9heBDXxkLowYEYix343Z3O1ng +Mej/8cKtssKqnNGDCrOORlR7UbqozsCaybUfUcJVc+chFP1oG/0232mroMqy1dqt +JgO09aS/Gm02rx1UMtpeE1B2OxudwvjH0vFdpY/AJ1Qt2nwINz2PlLRQXsvkswdc +kgtC2WLGpmih684KCBNgL9sY3xb0PW5XVv/QkBfTvQKBgQDf70vOk8TzX9OL4Zqr +y1/DxwY5LwJ6ltDFWA6akP4kazVvUEE8rtqAVlz7Raxvaec2CZN5dNpnBZeQvTKU +S0fr+/5BOnBaoT12IdKM0AMmViyTOuOqxVMxjFoQ814OqtrYvUegr8ZPCdTK1mJQ +GAgZkjpqvmIr74KsFx75QlIzqwKBgQDKgHTAwlcLS25sSXNqiE8i+p+sdRR7Bv2N +dMc6U+oQW6hKew/g37vvQudRONXVp4koTWEjKzNyOZ/YRwL6Fx3LAYFy+Jq/pKtT +jiOp32eWfC6WHv2aLw1G91dy0dqp3l5q2z36atvUxDa2Wll+mGazEtcrA1Rqbj3Z +Wo1fo71WGwKBgHQJBCfzy/8sLWrzKPlR9bp1m5Tv9gHduio3+cE/1mC6qMKYPGWc +WR5dIesV7EcDAkqu5Zru4Oi3LhVS5C2RYKA4QEQ+as+bc0SOPBK5CpjH2Gsl/aiU +fQpUpqrX4GoLQEFEuyPZURHNj1TXh7Pm7/OIIPsE0cvgXL6dcHBKXFvnAoGAKKdT +SSN64BybpYe1cQy+fnI8Ph4fJ3fGzXBFUvNnyTLtfU5paKbiDu2qjMbRPxxsT7gB +KVNR97uT2JKhCV48r/W0bEV2o8TGVHbzt/XO0QpLO/4qwZpymu2rE7UHphSrdd5f ++fcb/QILTd6jmuOzsn20zsDTYK6TIiCowyuXJkMCgYA71K+qiO/tz7Zv06U+Zbb4 +Cs4QKStqfJeDdsNBFmRBKhwGVvp0Ow++Q/ogbpsazApvVgLrlk4C3hnRhwEM/YDs +65/XQMX04RiERUR7y8ZOI/0pbL94m+ccdZ6QOkjFMPgo/XJJxGKrrlG8851qu2g3 +NSNqScFVHZTlk9RPO3FpPQ== +-----END PRIVATE KEY----- diff --git a/tests/integration/targets/azure_rm_keyvault/tasks/main.yml b/tests/integration/targets/azure_rm_keyvault/tasks/main.yml index ef6b53a4d..99e42be8e 100644 --- a/tests/integration/targets/azure_rm_keyvault/tasks/main.yml +++ b/tests/integration/targets/azure_rm_keyvault/tasks/main.yml @@ -4,6 +4,27 @@ tenant_id: "{{ azure_tenant }}" run_once: true +- name: Gather Resource Group info + azure.azcollection.azure_rm_resourcegroup_info: + name: "{{ resource_group }}" + register: __rg_info + +- name: Set location and managed_identity_ids + ansible.builtin.set_fact: + location: "eastus2" + # location: "{{ __rg_info.resourcegroups.0.location }}" + managed_identity_ids: [] + +- name: Create user managed identities + ansible.builtin.include_tasks: "{{ role_path }}/../../../integration_common_tasks/managed_identity.yml" + vars: + managed_identity_test_unique: 'hsmKeyVault' + managed_identity_unique: "{{ item }}" + managed_identity_action: 'create' + managed_identity_location: "{{ location }}" + with_items: + - '1' + - name: Lookup service principal object id ansible.builtin.set_fact: object_id: "{{ lookup('azure.azcollection.azure_service_principal_attribute', @@ -283,3 +304,235 @@ ansible.builtin.assert: that: - output.changed == false + +# +# azure_rm_keyvault hsm setup and test +# + +- name: Lookup service principal object id + ansible.builtin.set_fact: + object_id: "{{ lookup('azure.azcollection.azure_service_principal_attribute', + azure_client_id=azure_client_id, + azure_secret=azure_secret, + azure_tenant=tenant_id) }}" + register: object_id_facts + +- name: Create instance of HSM -- check mode + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + tenant_id: "{{ tenant_id }}" + administrators: + - "{{ object_id }}" + identity: + type: UserAssigned + user_assigned_identity: "{{ managed_identity_ids[0] }}" + soft_delete_retention_in_days: 7 + sku: + name: Standard_B1 + family: B + check_mode: true + register: output + +- name: Assert the resource instance is well created + ansible.builtin.assert: + that: + - output.changed + +- name: Create instance of HSM + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + tenant_id: "{{ tenant_id }}" + administrators: + - "{{ object_id }}" + identity: + type: UserAssigned + user_assigned_identity: "{{ managed_identity_ids[0] }}" + soft_delete_retention_in_days: 7 + sku: + name: Standard_B1 + family: B + register: output + +- name: Assert the resource instance is well created + ansible.builtin.assert: + that: + - output.changed + +- name: Create instance of HSM again + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + tenant_id: "{{ tenant_id }}" + administrators: + - "{{ object_id }}" + identity: + type: UserAssigned + user_assigned_identity: "{{ managed_identity_ids[0] }}" + soft_delete_retention_in_days: 7 + sku: + name: Standard_B1 + family: B + register: output + +- name: Assert the state has not changed + ansible.builtin.assert: + that: + - output.changed == false + +- name: Update existing HSM (add tags) + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + tenant_id: "{{ tenant_id }}" + administrators: + - "{{ object_id }}" + identity: + type: UserAssigned + user_assigned_identity: "{{ managed_identity_ids[0] }}" + soft_delete_retention_in_days: 7 + sku: + name: Standard_B1 + family: B + tags: + aaa: bbb + register: output + +- name: Assert the state has changed + ansible.builtin.assert: + that: + - output.changed == true + +- name: Get hsm facts + azure_rm_keyvault_info: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + register: facts + +- name: Assert the facts are properly set + ansible.builtin.assert: + that: + - facts['hsms'] | length == 1 + - facts['hsms'][0]['hsm_uri'] != None + - facts['hsms'][0]['name'] != None + - facts['hsms'][0]['sku'] != None + - facts['hsms'][0]['id'] != None + - facts['hsms'][0]['enable_soft_delete'] == true + - facts['hsms'][0]['soft_delete_retention_in_days'] == 7 + +- name: Download the security domain file + azure_rm_keyvaultsecuritydomain: + keyvault_uri: https://hsm{{ rpfx }}.managedhsm.azure.net + sd_quorum: 2 + sd_wrapping_keys: + - "{{ lookup('file', 'cert_0.cer') }}" + - "{{ lookup('file', 'cert_1.cer') }}" + - "{{ lookup('file', 'cert_2.cer') }}" + action: download + register: output + +- name: Save the security domain file + ansible.builtin.debug: + var: output["security_domain"] + +# +# azure_rm_hsmkey tests +# + +- name: Create a hsm key + block: + - name: Create a hsm key + azure_rm_keyvaultkey: + keyvault_uri: https://hsm{{ rpfx }}.managedhsm.azure.net + key_name: testkey + tags: + testing: test + delete: on-exit + register: output + - name: Assert the hsm key created + ansible.builtin.assert: + that: output.changed + rescue: + - name: Delete the hsm key + azure_rm_keyvaultkey: + keyvault_uri: https://hsm{{ rpfx }}.managedhsm.azure.net + state: absent + key_name: testkey + +- name: Get key current version + azure_rm_keyvaultkey_info: + vault_uri: https://hsm{{ rpfx }}.managedhsm.azure.net + name: testkey + register: facts + +- name: Assert hsm facts + ansible.builtin.assert: + that: + - facts['keys'] | length == 1 + - facts['keys'][0]['kid'] + - facts['keys'][0]['permitted_operations'] | length > 0 + - facts['keys'][0]['type'] + - facts['keys'][0]['version'] + +- name: Delete a hsm key + azure_rm_keyvaultkey: + keyvault_uri: https://hsm{{ rpfx }}.managedhsm.azure.net + state: absent + key_name: testkey + register: output + +- name: Assert the hsm deleted + ansible.builtin.assert: + that: output.changed + +# +# azure_rm_keyvault finalize & clean up +# + +- name: Delete instance of HSM -- check mode + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + state: absent + check_mode: true + register: output + +- name: Assert the state has changed + ansible.builtin.assert: + that: + - output.changed + +- name: Delete instance of HSM + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + state: absent + register: output + +- name: Assert the state has changed + ansible.builtin.assert: + that: + - output.changed + +- name: Delete unexisting instance of HSM + azure_rm_keyvault: + resource_group: "{{ resource_group }}" + hsm_name: "hsm{{ rpfx }}" + state: absent + register: output + +- name: Assert the state has changed + ansible.builtin.assert: + that: + - output.changed == false + +- name: Delete user managed identities + ansible.builtin.include_tasks: "{{ role_path }}/../../../integration_common_tasks/managed_identity.yml" + vars: + managed_identity_test_unique: 'hsmKeyVault' + managed_identity_unique: "{{ item }}" + managed_identity_action: 'delete' + managed_identity_location: "{{ location }}" + with_items: + - '1'