diff --git a/meta/runtime.yml b/meta/runtime.yml index 0de862ef237..d35813dee79 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -140,6 +140,7 @@ action_groups: - route53 - route53_health_check - route53_info + - route53_ksk - route53_zone - s3_bucket - s3_bucket_info diff --git a/plugins/modules/route53_ksk.py b/plugins/modules/route53_ksk.py new file mode 100644 index 00000000000..ba75755b4ee --- /dev/null +++ b/plugins/modules/route53_ksk.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +module: route53_ksk +short_description: Manages a key-signing key (KSK) +version_added: 9.2.0 +description: + - Creates a new key-signing key (KSK) associated with a hosted zone. + You can only have two KSKs per hosted zone. + - When O(state=absent), it deactivates and deletes a key-signing key (KSK). + - Activates a key-signing key (KSK) so that it can be used for signing by DNSSEC. + - Deactivates a key-signing key (KSK) so that it will not be used for signing by DNSSEC. +options: + state: + description: + - Whether or not the zone should exist or not. + default: present + choices: [ "present", "absent" ] + type: str + caller_reference: + description: + - A unique string that identifies the request. + - Required when O(state=present). + type: str + hosted_zone_id: + description: + - The unique string (ID) used to identify a hosted zone. + type: str + required: true + aliases: ["zone_id"] + key_management_service_arn: + description: + - The Amazon resource name (ARN) for a customer managed key in Key Management Service (KMS). + - Required when O(state=present). + type: str + aliases: ["kms_arn"] + name: + description: + - A string used to identify a key-signing key (KSK). + type: str + required: true + status: + description: + - A string specifying the initial status of the key-signing key (KSK). + - When O(state=presnent), you can set the value to V(ACTIVE) or V(INACTIVE). + type: str + default: "ACTIVE" + choices: ["ACTIVE", "INACTIVE"] + wait: + description: + - Wait until the changes have been replicated. + type: bool + default: false + wait_timeout: + description: + - How long to wait for the changes to be replicated, in seconds. + default: 300 + type: int +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +author: + - Alina Buzachis (@alinabuzachis) +""" + +EXAMPLES = r""" +- name: Create a Key Signing Key Request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + +- name: Activate a Key Signing Key Request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "ACTIVE" + state: present + +- name: Delete a Key Signing Key Request and deactivate it + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent +""" + +RETURN = r""" +change_info: + description: A dictionary that describes change information about changes made to the hosted zone. + returned: when a Key Signing Key is created or it exists and is updated/deleted + type: dict + contains: + id: + description: Change ID. + type: str + status: + description: The current state of the request. + type: str + submitted_at: + description: The date and time that the change request was submitted in ISO 8601 format and Coordinated Universal Time (UTC). + type: str + comment: + description: A comment you can provide. + type: str + sample: { + "id": "/change/C090307813XORZJ5J3U4", + "status": "PENDING", + "submitted_at": "2024-12-04T15:15:36.743000+00:00" + } +location: + description: The unique URL representing the new key-signing key (KSK). + returned: when only a new Key Signing Key is created + type: str + sample: "https://route53.amazonaws.com/2013-04-01/keysigningkey/xxx/ansible-test-ksk" +key_signing_key: + description: The key-signing key (KSK) that the request creates. + returned: always + type: dict + contains: + name: + description: A string used to identify a key-signing key (KSK). + type: str + kms_arn: + description: The Amazon resource name (ARN) used to identify the customer managed key in Key Management Service (KMS). + type: str + flag: + description: An integer that specifies how the key is used. + type: int + signing_algorithm_mnemonic: + description: A string used to represent the signing algorithm. + type: str + signing_algorithm_type: + description: An integer used to represent the signing algorithm. + type: int + digest_algorithm_mnemonic: + description: A string used to represent the delegation signer digest algorithm. + type: str + digest_algorithm_type: + description: An integer used to represent the delegation signer digest algorithm. + type: int + key_tag: + description: An integer used to identify the DNSSEC record for the domain name. + type: int + digest_value: + description: A cryptographic digest of a DNSKEY resource record (RR). + type: str + public_key: + description: The public key, represented as a Base64 encoding. + type: str + ds_record: + description: A string that represents a delegation signer (DS) record. + type: str + dnskey_record: + description: A string that represents a DNSKEY record. + type: str + status: + description: A string that represents the current key-signing key (KSK) status. + type: str + status_message: + description: The status message provided for ACTION_NEEDED or INTERNAL_FAILURE statuses. + type: str + created_date: + description: The date when the key-signing key (KSK) was created. + type: str + last_modified_date: + description: The last time that the key-signing key (KSK) was changed. + type: str + sample: { + "created_date": "2024-12-04T15:15:36.715000+00:00", + "digest_algorithm_mnemonic": "SHA-256", + "digest_algorithm_type": 2, + "digest_value": "xxx", + "dnskey_record": "xxx", + "ds_record": "xxx", + "flag": 257, + "key_tag": 18948, + "kms_arn": "arn:aws:kms:us-east-1:xxx:key/xxx", + "last_modified_date": "2024-12-04T15:15:36.715000+00:00", + "name": "ansible-test-44230979--ksk", + "public_key": "xxxx", + "signing_algorithm_mnemonic": "ECDSAP256SHA256", + "signing_algorithm_type": 13, + "status": "INACTIVE" + } +""" + + +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.exceptions import AnsibleAWSError +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.waiters import get_waiter + + +def deactivate(client, hosted_zone_id, name): + return client.deactivate_key_signing_key(HostedZoneId=hosted_zone_id, Name=name) + + +def activate(client, hosted_zone_id, name): + return client.activate_key_signing_key(HostedZoneId=hosted_zone_id, Name=name) + + +def get_change(client, change_id): + return client.get_change(Id=change_id) + + +def get_hosted_zone(client, hosted_zone_id): + return client.get_hosted_zone(Id=hosted_zone_id) + + +def get_dnssec(client, hosted_zone_id): + return client.get_dnssec(HostedZoneId=hosted_zone_id) + + +def find_ksk(client, module): + hosted_zone_dnssec = get_dnssec(client, module.params.get("hosted_zone_id")) + if hosted_zone_dnssec["KeySigningKeys"] != []: + for ksk in hosted_zone_dnssec["KeySigningKeys"]: + if ksk["Name"] == module.params.get("name"): + return ksk + return {} + + +def wait(client, module, change_id): + try: + waiter = get_waiter(client, "resource_record_sets_changed") + waiter.wait( + Id=change_id, + WaiterConfig=dict( + Delay=5, + MaxAttempts=module.params.get("wait_timeout") // 5, + ), + ) + except botocore.exceptions.WaiterError as e: + module.fail_json_aws(e, msg="Timeout waiting for changes to be applied") + + +def create_or_update(client, module: AnsibleAWSModule, ksk): + changed: bool = False + zone_id = module.params.get("hosted_zone_id") + name = module.params.get("name") + status = module.params.get("status") + response = {} + + if ksk: + if ksk["Status"] != status: + changed = True + + if module.check_mode: + ksk["Status"] = status + module.exit_json( + changed=changed, + key_signing_key=camel_dict_to_snake_dict(ksk), + msg=f"Would have updated the Key Signing Key status to {status} if not in check_mode.", + ) + + if status == "ACTIVE": + response.update(activate(client, zone_id, name)) + elif status == "INACTIVE": + response.update(deactivate(client, zone_id, name)) + else: + changed = True + if module.check_mode: + module.exit_json(changed=changed, msg="Would have created the Key Signing Key if not in check_mode.") + + response = client.create_key_signing_key( + CallerReference=module.params.get("caller_reference"), + KeyManagementServiceArn=module.params.get("key_management_service_arn"), + HostedZoneId=zone_id, + Name=name, + Status=status, + ) + + return changed, response + + +def delete(client, module: AnsibleAWSModule, ksk): + changed: bool = False + zone_id = module.params.get("hosted_zone_id") + name = module.params.get("name") + response = {} + + if ksk: + changed = True + if module.check_mode: + module.exit_json( + changed=changed, + key_signing_key={}, + msg="Would have deleted the Key Signing Key if not in check_mode.", + ) + + result = deactivate(client, zone_id, name) + change_id = result["ChangeInfo"]["Id"] + wait(client, module, change_id) + + response.update(client.delete_key_signing_key(HostedZoneId=zone_id, Name=name)) + + return changed, response + + +def main() -> None: + argument_spec = dict( + caller_reference=dict(type="str"), + hosted_zone_id=dict(type="str", aliases=["zone_id"], required=True), + key_management_service_arn=dict(type="str", aliases=["kms_arn"], no_log=False), + name=dict(type="str", required=True), + status=dict(type="str", default="ACTIVE", choices=["ACTIVE", "INACTIVE"]), + state=dict(default="present", choices=["present", "absent"]), + wait=dict(type="bool", default=False), + wait_timeout=dict(type="int", default=300), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[["state", "present", ["caller_reference", "key_management_service_arn"]]], + ) + + try: + client = module.client("route53") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to connect to AWS.") + + changed = False + state = module.params.get("state") + + try: + ksk = find_ksk(client, module) + if state == "present": + changed, result = create_or_update(client, module, ksk) + else: + changed, result = delete(client, module, ksk) + + if module.params.get("wait") and result.get("ChangeInfo"): + change_id = result["ChangeInfo"]["Id"] + wait(client, module, change_id) + result.update(get_change(client, change_id)) + + # Get updated information about KSK + result["KeySigningKey"] = find_ksk(client, module) + + if "ResponseMetadata" in result: + del result["ResponseMetadata"] + + except AnsibleAWSError as e: + module.fail_json_aws_error(e) + + module.exit_json(changed=changed, **camel_dict_to_snake_dict(result)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/route53_ksk/aliases b/tests/integration/targets/route53_ksk/aliases new file mode 100644 index 00000000000..4ef4b2067d0 --- /dev/null +++ b/tests/integration/targets/route53_ksk/aliases @@ -0,0 +1 @@ +cloud/aws diff --git a/tests/integration/targets/route53_ksk/tasks/main.yml b/tests/integration/targets/route53_ksk/tasks/main.yml new file mode 100644 index 00000000000..1cd9a1c9cc6 --- /dev/null +++ b/tests/integration/targets/route53_ksk/tasks/main.yml @@ -0,0 +1,273 @@ +--- +- name: Integration tests for the route53_ksk module + module_defaults: + group/aws: + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + session_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + block: + - name: Get ARN of calling user + amazon.aws.aws_caller_info: + register: aws_caller_info + + - name: Create a KMS key + amazon.aws.kms_key: + alias: "{{ resource_prefix }}-kms-route53" + policy: "{{ lookup('template', 'kms_policy.j2') }}" + state: present + enabled: true + customer_master_key_spec: "ECC_NIST_P256" + key_usage: "SIGN_VERIFY" + register: kms_key + + - name: Create a Route53 public zone + amazon.aws.route53_zone: + zone: "{{ resource_prefix }}.public" + comment: "Route53 Zone for KSK Testing" + state: present + register: _hosted_zone + + - name: Create a Key Signing Key request (check_mode) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + check_mode: true + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - _ksk_request.changed + - '"msg" in _ksk_request' + - '"Would have created the Key Signing Key if not in check_mode." in _ksk_request.msg' + + - name: Create a Key Signing Key request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - '"change_info" in _ksk_request' + - _ksk_request.changed + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.status == "INACTIVE" + - _ksk_request.change_info.status == "PENDING" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Create a Key Signing Key request (idempotency) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - not _ksk_request.changed + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.status == "INACTIVE" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Activate the Key Signing Key request (check_mode) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "ACTIVE" + state: present + check_mode: true + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - _ksk_request.changed + - '"msg" in _ksk_request' + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + - '"Would have updated the Key Signing Key status to ACTIVE if not in check_mode." in _ksk_request.msg' + + - name: Activate the Key Signing Key request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "ACTIVE" + state: present + wait: true + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - '"change_info" in _ksk_request' + - _ksk_request.changed + - _ksk_request.key_signing_key.status == "ACTIVE" + - _ksk_request.change_info.status == "INSYNC" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Activate the Key Signing Key request (idempotency) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "ACTIVE" + state: present + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - not _ksk_request.changed + - _ksk_request.key_signing_key.status == "ACTIVE" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Deactivate the Key Signing Key request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + wait: true + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - '"change_info" in _ksk_request' + - _ksk_request.changed + - _ksk_request.key_signing_key.status == "INACTIVE" + - _ksk_request.change_info.status == "INSYNC" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Deactivate the Key Signing Key request (idempotency) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + key_management_service_arn: "{{ kms_key.key_arn }}" + caller_reference: "{{ aws_caller_info.arn }}" + status: "INACTIVE" + state: present + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - not _ksk_request.changed + - _ksk_request.key_signing_key.status == "INACTIVE" + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key.name == resource_prefix + '-ksk' + + - name: Delete the Key Signing Key request (check_mode) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent + check_mode: true + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - _ksk_request.changed + - '"msg" in _ksk_request' + - '"key_signing_key" in _ksk_request' + - _ksk_request.key_signing_key == {} + - '"Would have deleted the Key Signing Key if not in check_mode." in _ksk_request.msg' + + - name: Delete the Key Signing Key request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - '"change_info" in _ksk_request' + - _ksk_request.changed + - _ksk_request.change_info.status == "PENDING" + + - name: Delete the Key Signing Key request (idempotency) + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - not _ksk_request.changed + + - name: Delete a not existing Key Signing Key request + amazon.aws.route53_ksk: + name: "not existing ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent + register: _ksk_request + + - name: Assert success + ansible.builtin.assert: + that: + - _ksk_request is successful + - not _ksk_request.changed + + always: + - name: Delete the Key Signing Key Request + amazon.aws.route53_ksk: + name: "{{ resource_prefix }}-ksk" + hosted_zone_id: "{{ _hosted_zone.zone_id }}" + state: absent + ignore_errors: true + + - name: Delete the Route53 public zone + amazon.aws.route53_zone: + zone: "{{ resource_prefix }}.public" + state: absent + ignore_errors: true + + - name: Delete the KSM key + amazon.aws.kms_key: + state: absent + alias: "{{ resource_prefix }}-kms-route53" + pending_window: 7 + ignore_errors: true diff --git a/tests/integration/targets/route53_ksk/templates/kms_policy.j2 b/tests/integration/targets/route53_ksk/templates/kms_policy.j2 new file mode 100644 index 00000000000..4b7a8bbf473 --- /dev/null +++ b/tests/integration/targets/route53_ksk/templates/kms_policy.j2 @@ -0,0 +1,29 @@ +{ + "Id": "dnssec-policy", + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Allow Route 53 DNSSEC Service", + "Effect": "Allow", + "Principal": { + "Service": "dnssec-route53.amazonaws.com" + }, + "Action": [ + "kms:DescribeKey", + "kms:GetPublicKey", + "kms:Sign", + "kms:Verify" + ], + "Resource": "*" + }, + { + "Sid": "Enable IAM User Permissions", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{{ aws_caller_info.account }}:root" + }, + "Action": "kms:*", + "Resource": "*" + } + ] +}