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