Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

route53_ksk - new module #2412

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ action_groups:
- route53
- route53_health_check
- route53_info
- route53_ksk
- route53_zone
- s3_bucket
- s3_bucket_info
Expand Down
361 changes: 361 additions & 0 deletions plugins/modules/route53_ksk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
#!/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.
- Deletes a key-signing key (KSK). Before you can delete a KSK, you must deactivate it.
The KSK must be deactivated before you can delete it regardless of whether the hosted
zone is enabled for DNSSEC signing.
- 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Whether or not the zone should exist or not.
- Whether or not the zone should exist.

default: present
choices: [ "present", "absent" ]
type: str
caller_reference:
description:
- A unique string that identifies the request.
alinabuzachis marked this conversation as resolved.
Show resolved Hide resolved
- 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).
alinabuzachis marked this conversation as resolved.
Show resolved Hide resolved
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).
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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hosted_zone_id: "{{ _hosted_zone.zone_id }}"
hosted_zone_id: "ZZZ1111112222"

key_management_service_arn: "{{ kms_key.key_arn }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
key_management_service_arn: "{{ kms_key.key_arn }}"
key_management_service_arn: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"

caller_reference: "{{ aws_caller_info.arn }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caller_reference: "{{ aws_caller_info.arn }}"
caller_reference: "arn:aws:iam::123456789012:user/SomeUser"

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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hosted_zone_id: "{{ _hosted_zone.zone_id }}"
hosted_zone_id: "ZZZ1111112222"

key_management_service_arn: "{{ kms_key.key_arn }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
key_management_service_arn: "{{ kms_key.key_arn }}"
key_management_service_arn: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"

caller_reference: "{{ aws_caller_info.arn }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
caller_reference: "{{ aws_caller_info.arn }}"
caller_reference: "arn:aws:iam::123456789012:user/SomeUser"

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 }}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hosted_zone_id: "{{ _hosted_zone.zone_id }}"
hosted_zone_id: "ZZZ1111112222"

status: "INACTIVE"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is status needed when we delete?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not. However the module was saying that the user need to deactivate the KSK before deleting. I changed the behaviour and not it deactivates by default when the KSK need to be deleted.

state: absent
"""

RETURN = r"""
change_info:
description: A dictionary that describes change information about changes made to the hosted zone.
returned: when the Key Signing Request is created or updated
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 None


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")

if ksk is not None:
response = {"KeySigningKey": ksk}
alinabuzachis marked this conversation as resolved.
Show resolved Hide resolved
if ksk["Status"] != status:
changed = True

if module.check_mode:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ksk["Status"] = status

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason as the delete operation, when the resource is actually updated, the value of ksk["Status"] will be modified or set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, done although as mentioned below there is the informational message saying that the status would have been updated to ACTIVE/INACTIVE if it was not in control mode.
In general, I don't really know if we tweak the retuned result of the resource in check mode to reflect the desired updates though. If we do so, the msg probably becomes useless... but I don't know.

module.exit_json(
changed=changed,
msg=f"Would have updated the Key Signing Key status to {status} if not in check_mode.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we return the changed dict (would have changed if not check_mode = true) as well?

)

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.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we return the changed dict (would have changed if not check_mode = true) as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but I don't understand what you mean. Can you please elaborate? changed is a boolean.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the confusion in my previous statement. What I meant was: in addition to the 'changed' status, do you think it's possible to also return the "response/return dictionary," even if no changes are made to the resource? By definition, any module designed to support 'check mode' reports what changes would have been made without actually applying them. However, this approach is not followed by our cloud modules. I was wondering if we could start implementing this behavior in new modules and then work on modifying the existing ones later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, understood, done!


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,
)

del response["ResponseMetadata"]

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 = {"KeySigningRequest": {}}

if ksk is not None:
changed = True
response["KeySigningRequest"] = ksk
alinabuzachis marked this conversation as resolved.
Show resolved Hide resolved
if module.check_mode:
module.exit_json(changed=changed, msg="Would have deleted the Key Signing Key if not in check_mode.")

if module.params.get("status") == "INACTIVE":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for a condition to have state = "delete" and status = "ACTIVE"? I'm not sure if such a scenario can exist. Do we need this condition? We can ignore status for deletion. I think we should deactivate the key signing key before deleting in any case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This situation was already handled because there was a note in the description asking the user to deactivate the key before deleting it. However, since it is not possible to delete a key without first deactivating it, eliminating an additional step or the need to specify the status: INACTIVE, I handled deactivation by default when the user wants to delete it.

result = deactivate(client, zone_id, name)
change_id = result["ChangeInfo"]["Id"]
wait(client, module, change_id)

response = 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["ChangeInfo"] = get_change(client, change_id)

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()
1 change: 1 addition & 0 deletions tests/integration/targets/route53_ksk/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cloud/aws
Loading
Loading