1#!/usr/local/bin/python3.8 2# 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8 9DOCUMENTATION = ''' 10--- 11module: aws_kms_info 12version_added: 1.0.0 13short_description: Gather information about AWS KMS keys 14description: 15 - Gather information about AWS KMS keys including tags and grants 16 - This module was called C(aws_kms_facts) before Ansible 2.9. The usage did not change. 17author: "Will Thames (@willthames)" 18options: 19 alias: 20 description: 21 - Alias for key. 22 - Mutually exclusive with I(key_id) and I(filters). 23 required: false 24 aliases: 25 - key_alias 26 type: str 27 version_added: 1.4.0 28 key_id: 29 description: 30 - Key ID or ARN of the key. 31 - Mutually exclusive with I(alias) and I(filters). 32 required: false 33 aliases: 34 - key_arn 35 type: str 36 version_added: 1.4.0 37 filters: 38 description: 39 - A dict of filters to apply. Each dict item consists of a filter key and a filter value. 40 The filters aren't natively supported by boto3, but are supported to provide similar 41 functionality to other modules. Standard tag filters (C(tag-key), C(tag-value) and 42 C(tag:tagName)) are available, as are C(key-id) and C(alias) 43 - Mutually exclusive with I(alias) and I(key_id). 44 type: dict 45 pending_deletion: 46 description: Whether to get full details (tags, grants etc.) of keys pending deletion 47 default: False 48 type: bool 49extends_documentation_fragment: 50- amazon.aws.aws 51- amazon.aws.ec2 52 53''' 54 55EXAMPLES = ''' 56# Note: These examples do not set authentication details, see the AWS Guide for details. 57 58# Gather information about all KMS keys 59- community.aws.aws_kms_info: 60 61# Gather information about all keys with a Name tag 62- community.aws.aws_kms_info: 63 filters: 64 tag-key: Name 65 66# Gather information about all keys with a specific name 67- community.aws.aws_kms_info: 68 filters: 69 "tag:Name": Example 70''' 71 72RETURN = ''' 73keys: 74 description: list of keys 75 type: complex 76 returned: always 77 contains: 78 key_id: 79 description: ID of key 80 type: str 81 returned: always 82 sample: abcd1234-abcd-1234-5678-ef1234567890 83 key_arn: 84 description: ARN of key 85 type: str 86 returned: always 87 sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 88 key_state: 89 description: The state of the key 90 type: str 91 returned: always 92 sample: PendingDeletion 93 key_usage: 94 description: The cryptographic operations for which you can use the key. 95 type: str 96 returned: always 97 sample: ENCRYPT_DECRYPT 98 origin: 99 description: 100 The source of the key's key material. When this value is C(AWS_KMS), 101 AWS KMS created the key material. When this value is C(EXTERNAL), the 102 key material was imported or the CMK lacks key material. 103 type: str 104 returned: always 105 sample: AWS_KMS 106 aws_account_id: 107 description: The AWS Account ID that the key belongs to 108 type: str 109 returned: always 110 sample: 1234567890123 111 creation_date: 112 description: Date of creation of the key 113 type: str 114 returned: always 115 sample: "2017-04-18T15:12:08.551000+10:00" 116 description: 117 description: Description of the key 118 type: str 119 returned: always 120 sample: "My Key for Protecting important stuff" 121 enabled: 122 description: Whether the key is enabled. True if C(KeyState) is true. 123 type: str 124 returned: always 125 sample: false 126 enable_key_rotation: 127 description: Whether the automatically key rotation every year is enabled. Returns None if key rotation status can't be determined. 128 type: bool 129 returned: always 130 sample: false 131 aliases: 132 description: list of aliases associated with the key 133 type: list 134 returned: always 135 sample: 136 - aws/acm 137 - aws/ebs 138 tags: 139 description: dictionary of tags applied to the key. Empty when access is denied even if there are tags. 140 type: dict 141 returned: always 142 sample: 143 Name: myKey 144 Purpose: protecting_stuff 145 policies: 146 description: list of policy documents for the keys. Empty when access is denied even if there are policies. 147 type: list 148 returned: always 149 sample: 150 Version: "2012-10-17" 151 Id: "auto-ebs-2" 152 Statement: 153 - Sid: "Allow access through EBS for all principals in the account that are authorized to use EBS" 154 Effect: "Allow" 155 Principal: 156 AWS: "*" 157 Action: 158 - "kms:Encrypt" 159 - "kms:Decrypt" 160 - "kms:ReEncrypt*" 161 - "kms:GenerateDataKey*" 162 - "kms:CreateGrant" 163 - "kms:DescribeKey" 164 Resource: "*" 165 Condition: 166 StringEquals: 167 kms:CallerAccount: "111111111111" 168 kms:ViaService: "ec2.ap-southeast-2.amazonaws.com" 169 - Sid: "Allow direct access to key metadata to the account" 170 Effect: "Allow" 171 Principal: 172 AWS: "arn:aws:iam::111111111111:root" 173 Action: 174 - "kms:Describe*" 175 - "kms:Get*" 176 - "kms:List*" 177 - "kms:RevokeGrant" 178 Resource: "*" 179 grants: 180 description: list of grants associated with a key 181 type: complex 182 returned: always 183 contains: 184 constraints: 185 description: Constraints on the encryption context that the grant allows. 186 See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) for further details 187 type: dict 188 returned: always 189 sample: 190 encryption_context_equals: 191 "aws:lambda:_function_arn": "arn:aws:lambda:ap-southeast-2:012345678912:function:xyz" 192 creation_date: 193 description: Date of creation of the grant 194 type: str 195 returned: always 196 sample: "2017-04-18T15:12:08+10:00" 197 grant_id: 198 description: The unique ID for the grant 199 type: str 200 returned: always 201 sample: abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234 202 grantee_principal: 203 description: The principal that receives the grant's permissions 204 type: str 205 returned: always 206 sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz 207 issuing_account: 208 description: The AWS account under which the grant was issued 209 type: str 210 returned: always 211 sample: arn:aws:iam::01234567890:root 212 key_id: 213 description: The key ARN to which the grant applies. 214 type: str 215 returned: always 216 sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890 217 name: 218 description: The friendly name that identifies the grant 219 type: str 220 returned: always 221 sample: xyz 222 operations: 223 description: The list of operations permitted by the grant 224 type: list 225 returned: always 226 sample: 227 - Decrypt 228 - RetireGrant 229 retiring_principal: 230 description: The principal that can retire the grant 231 type: str 232 returned: always 233 sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz 234''' 235 236 237try: 238 import botocore 239except ImportError: 240 pass # Handled by AnsibleAWSModule 241 242from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict 243 244from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule 245from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code 246from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry 247from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict 248 249# Caching lookup for aliases 250_aliases = dict() 251 252 253@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 254def get_kms_keys_with_backoff(connection): 255 paginator = connection.get_paginator('list_keys') 256 return paginator.paginate().build_full_result() 257 258 259@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 260def get_kms_aliases_with_backoff(connection): 261 paginator = connection.get_paginator('list_aliases') 262 return paginator.paginate().build_full_result() 263 264 265def get_kms_aliases_lookup(connection): 266 if not _aliases: 267 for alias in get_kms_aliases_with_backoff(connection)['Aliases']: 268 # Not all aliases are actually associated with a key 269 if 'TargetKeyId' in alias: 270 # strip off leading 'alias/' and add it to key's aliases 271 if alias['TargetKeyId'] in _aliases: 272 _aliases[alias['TargetKeyId']].append(alias['AliasName'][6:]) 273 else: 274 _aliases[alias['TargetKeyId']] = [alias['AliasName'][6:]] 275 return _aliases 276 277 278@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 279def get_kms_tags_with_backoff(connection, key_id, **kwargs): 280 return connection.list_resource_tags(KeyId=key_id, **kwargs) 281 282 283@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 284def get_kms_grants_with_backoff(connection, key_id, **kwargs): 285 params = dict(KeyId=key_id) 286 if kwargs.get('tokens'): 287 params['GrantTokens'] = kwargs['tokens'] 288 paginator = connection.get_paginator('list_grants') 289 return paginator.paginate(**params).build_full_result() 290 291 292@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 293def get_kms_metadata_with_backoff(connection, key_id): 294 return connection.describe_key(KeyId=key_id) 295 296 297@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 298def list_key_policies_with_backoff(connection, key_id): 299 paginator = connection.get_paginator('list_key_policies') 300 return paginator.paginate(KeyId=key_id).build_full_result() 301 302 303@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 304def get_key_policy_with_backoff(connection, key_id, policy_name): 305 return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name) 306 307 308@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) 309def get_enable_key_rotation_with_backoff(connection, key_id): 310 try: 311 current_rotation_status = connection.get_key_rotation_status(KeyId=key_id) 312 except is_boto3_error_code(['AccessDeniedException', 'UnsupportedOperationException']) as e: 313 return None 314 315 return current_rotation_status.get('KeyRotationEnabled') 316 317 318def canonicalize_alias_name(alias): 319 if alias is None: 320 return None 321 if alias.startswith('alias/'): 322 return alias 323 return 'alias/' + alias 324 325 326def get_kms_tags(connection, module, key_id): 327 # Handle pagination here as list_resource_tags does not have 328 # a paginator 329 kwargs = {} 330 tags = [] 331 more = True 332 while more: 333 try: 334 tag_response = get_kms_tags_with_backoff(connection, key_id, **kwargs) 335 tags.extend(tag_response['Tags']) 336 except is_boto3_error_code('AccessDeniedException'): 337 tag_response = {} 338 except botocore.exceptions.ClientError as e: # pylint: disable=duplicate-except 339 module.fail_json_aws(e, msg="Failed to obtain key tags") 340 if tag_response.get('NextMarker'): 341 kwargs['Marker'] = tag_response['NextMarker'] 342 else: 343 more = False 344 return tags 345 346 347def get_kms_policies(connection, module, key_id): 348 try: 349 policies = list_key_policies_with_backoff(connection, key_id)['PolicyNames'] 350 return [get_key_policy_with_backoff(connection, key_id, policy)['Policy'] for 351 policy in policies] 352 except is_boto3_error_code('AccessDeniedException'): 353 return [] 354 except botocore.exceptions.ClientError as e: # pylint: disable=duplicate-except 355 module.fail_json_aws(e, msg="Failed to obtain key policies") 356 357 358def key_matches_filter(key, filtr): 359 if filtr[0] == 'key-id': 360 return filtr[1] == key['key_id'] 361 if filtr[0] == 'tag-key': 362 return filtr[1] in key['tags'] 363 if filtr[0] == 'tag-value': 364 return filtr[1] in key['tags'].values() 365 if filtr[0] == 'alias': 366 return filtr[1] in key['aliases'] 367 if filtr[0].startswith('tag:'): 368 tag_key = filtr[0][4:] 369 if tag_key not in key['tags']: 370 return False 371 return key['tags'].get(tag_key) == filtr[1] 372 373 374def key_matches_filters(key, filters): 375 if not filters: 376 return True 377 else: 378 return all([key_matches_filter(key, filtr) for filtr in filters.items()]) 379 380 381def get_key_details(connection, module, key_id, tokens=None): 382 if not tokens: 383 tokens = [] 384 try: 385 result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata'] 386 # Make sure we have the canonical ARN, we might have been passed an alias 387 key_id = result['Arn'] 388 except is_boto3_error_code('NotFoundException'): 389 return None 390 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except 391 module.fail_json_aws(e, msg="Failed to obtain key metadata") 392 result['KeyArn'] = result.pop('Arn') 393 394 try: 395 aliases = get_kms_aliases_lookup(connection) 396 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 397 module.fail_json_aws(e, msg="Failed to obtain aliases") 398 # We can only get aliases for our own account, so we don't need the full ARN 399 result['aliases'] = aliases.get(result['KeyId'], []) 400 result['enable_key_rotation'] = get_enable_key_rotation_with_backoff(connection, key_id) 401 402 if module.params.get('pending_deletion'): 403 return camel_dict_to_snake_dict(result) 404 405 try: 406 result['grants'] = get_kms_grants_with_backoff(connection, key_id, tokens=tokens)['Grants'] 407 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 408 module.fail_json_aws(e, msg="Failed to obtain key grants") 409 tags = get_kms_tags(connection, module, key_id) 410 411 result = camel_dict_to_snake_dict(result) 412 result['tags'] = boto3_tag_list_to_ansible_dict(tags, 'TagKey', 'TagValue') 413 result['policies'] = get_kms_policies(connection, module, key_id) 414 return result 415 416 417def get_kms_info(connection, module): 418 if module.params.get('key_id'): 419 key_id = module.params.get('key_id') 420 details = get_key_details(connection, module, key_id) 421 if details: 422 return [details] 423 return [] 424 elif module.params.get('alias'): 425 alias = canonicalize_alias_name(module.params.get('alias')) 426 details = get_key_details(connection, module, alias) 427 if details: 428 return [details] 429 return [] 430 else: 431 try: 432 keys = get_kms_keys_with_backoff(connection)['Keys'] 433 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 434 module.fail_json_aws(e, msg="Failed to obtain keys") 435 return [get_key_details(connection, module, key['KeyId']) for key in keys] 436 437 438def main(): 439 argument_spec = dict( 440 alias=dict(aliases=['key_alias']), 441 key_id=dict(aliases=['key_arn']), 442 filters=dict(type='dict'), 443 pending_deletion=dict(type='bool', default=False), 444 ) 445 446 module = AnsibleAWSModule(argument_spec=argument_spec, 447 mutually_exclusive=[['alias', 'filters', 'key_id']], 448 supports_check_mode=True) 449 if module._name == 'aws_kms_facts': 450 module.deprecate("The 'aws_kms_facts' module has been renamed to 'aws_kms_info'", date='2021-12-01', collection_name='community.aws') 451 452 try: 453 connection = module.client('kms') 454 except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 455 module.fail_json_aws(e, msg='Failed to connect to AWS') 456 457 all_keys = get_kms_info(connection, module) 458 module.exit_json(keys=[key for key in all_keys if key_matches_filters(key, module.params['filters'])]) 459 460 461if __name__ == '__main__': 462 main() 463