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_zone - Add support for enabling DNSSEC signing in a specific hosted zone #2421

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/20241203-route53-dnssec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- route53_zone - Add support for enabling DNSSEC signing in a specific hosted zone (https://github.com/ansible-collections/amazon.aws/issues/1976).
223 changes: 189 additions & 34 deletions plugins/modules/route53_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
- The reusable delegation set ID to be associated with the zone.
- Note that you can't associate a reusable delegation set with a private hosted zone.
type: str
dnssec:
description:
- Enables DNSSEC signing in a specific hosted zone.
type: bool
default: false
version_added: 9.2.0
extends_documentation_fragment:
- amazon.aws.common.modules
- amazon.aws.region.modules
Expand Down Expand Up @@ -178,14 +184,106 @@
returned: for public hosted zones, if they have been associated with a reusable delegation set
type: str
sample: "A1BCDEF2GHIJKL"
dnssec:
description: Information about DNSSEC for a specific hosted zone.
returned: when O(state=present) and the hosted zone is public
version_added: 9.2.0
type: dict
contains:
key_signing_key:
description: The key-signing key (KSK) that the request creates.
returned: when O(state=present)
type: list
elements: 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"
}]
status:
description: A dictionary representing the status of DNSSEC.
type: dict
contains:
serve_signature:
description: A string that represents the current hosted zone signing status.
type: str
sample: {
"serve_signature": "SIGNING"
}
tags:
description: Tags associated with the zone.
returned: when tags are defined
type: dict
"""

import time
from typing import Any
from typing import Dict

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict

from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.route53 import get_tags
Expand Down Expand Up @@ -222,6 +320,68 @@ def find_zones(zone_in, private_zone):
return zones


def get_dnssec(hosted_zone_id: str) -> Dict[str, Any]:
try:
return client.get_dnssec(HostedZoneId=hosted_zone_id)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get dnssec details about hosted zone {hosted_zone_id}")


def enable_hosted_zone_dnssec(zone_id: str) -> None:
try:
client.enable_hosted_zone_dnssec(HostedZoneId=zone_id)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not enable DNSSEC for {zone_id}")


def disable_hosted_zone_dnssec(zone_id: str) -> None:
try:
client.disable_hosted_zone_dnssec(HostedZoneId=zone_id)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not enable DNSSEC for {zone_id}")


def get_hosted_zone(hosted_zone_id: str) -> Dict[str, Any]:
try:
return client.get_hosted_zone(Id=hosted_zone_id) # could be in different regions or have different VPCids
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get details about hosted zone {hosted_zone_id}")


def ensure_dnssec(zone_id: str) -> bool:
changed = False
dnssec = module.params.get("dnssec")

response = get_dnssec(zone_id)
dnssec_status = response["Status"]["ServeSignature"]

# If get_dnssec command output returns "NOT_SIGNING",
# the Domain Name System Security Extensions (DNSSEC) signing is not enabled for the
# Amazon Route 53 hosted zone.
if dnssec:
if dnssec_status == "NOT_SIGNING":
# Enable DNSSEC
if not module.check_mode:
enable_hosted_zone_dnssec(zone_id)
changed = True
elif dnssec_status == "DELETING":
# DNSSEC signing is in the process of being removed for the hosted zone.
module.warn(
f"DNSSEC signing is in the process of being removed for the hosted zone: {zone_id}."
"Could not enable it."
)
else:
if dnssec_status == "SIGNING":
# Disable DNSSEC
if not module.check_mode:
disable_hosted_zone_dnssec(zone_id)
changed = True
# if dnssec_status == "DELETING":
# DNSSEC signing is in the process of being removed for the hosted zone.
Comment on lines +379 to +380
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
# if dnssec_status == "DELETING":
# DNSSEC signing is in the process of being removed for the hosted zone.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added these comments to document what's happening if dnssec_status == "DELETING":.


return changed


def create(matching_zones):
zone_in = module.params.get("zone").lower()
vpc_id = module.params.get("vpc_id")
Expand Down Expand Up @@ -255,6 +415,15 @@ def create(matching_zones):

zone_id = result.get("zone_id")
if zone_id:
if not private_zone:
# Enable/Disable DNSSEC
changed |= ensure_dnssec(zone_id)

# Update result with information about DNSSEC
result["dnssec"] = camel_dict_to_snake_dict(get_dnssec(zone_id))
del result["dnssec"]["response_metadata"]

# Handle Tags
if tags is not None:
changed |= manage_tags(module, client, "hostedzone", zone_id, tags, purge_tags)
result["tags"] = get_tags(module, client, "hostedzone", zone_id)
Expand All @@ -266,10 +435,7 @@ def create(matching_zones):

def create_or_update_private(matching_zones, record):
for z in matching_zones:
try:
result = client.get_hosted_zone(Id=z["Id"]) # could be in different regions or have different VPCids
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get details about hosted zone {z['Id']}")
result = get_hosted_zone(z["Id"]) # could be in different regions or have different VPCids
zone_details = result["HostedZone"]
vpc_details = result["VPCs"]
matching = False
Expand Down Expand Up @@ -340,12 +506,9 @@ def create_or_update_private(matching_zones, record):
def create_or_update_public(matching_zones, record):
zone_details, zone_delegation_set_details = None, {}
for matching_zone in matching_zones:
try:
zone = client.get_hosted_zone(Id=matching_zone["Id"])
zone_details = zone["HostedZone"]
zone_delegation_set_details = zone.get("DelegationSet", {})
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get details about hosted zone {matching_zone['Id']}")
zone = get_hosted_zone(matching_zone["Id"])
zone_details = zone["HostedZone"]
zone_delegation_set_details = zone.get("DelegationSet", {})
if "Comment" in zone_details["Config"] and zone_details["Config"]["Comment"] != record["comment"]:
if not module.check_mode:
try:
Expand Down Expand Up @@ -393,30 +556,21 @@ def create_or_update_public(matching_zones, record):

def delete_private(matching_zones, vpcs):
for z in matching_zones:
try:
result = client.get_hosted_zone(Id=z["Id"])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get details about hosted zone {z['Id']}")
result = get_hosted_zone(z["Id"])
zone_details = result["HostedZone"]
vpc_details = result["VPCs"]
if isinstance(vpc_details, dict):
if vpc_details["VPC"]["VPCId"] == vpcs[0]["id"] and vpcs[0]["region"] == vpc_details["VPC"]["VPCRegion"]:
if not module.check_mode:
try:
client.delete_hosted_zone(Id=z["Id"])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not delete hosted zone {z['Id']}")
delete_hosted_zone(z["Id"])
return True, f"Successfully deleted {zone_details['Name']}"
else:
# Sort the lists and compare them to make sure they contain the same items
if sorted([vpc["id"] for vpc in vpcs]) == sorted([v["VPCId"] for v in vpc_details]) and sorted(
[vpc["region"] for vpc in vpcs]
) == sorted([v["VPCRegion"] for v in vpc_details]):
if not module.check_mode:
try:
client.delete_hosted_zone(Id=z["Id"])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not delete hosted zone {z['Id']}")
delete_hosted_zone(z["Id"])
return True, f"Successfully deleted {zone_details['Name']}"

return False, "The VPCs do not match a private hosted zone."
Expand All @@ -428,10 +582,7 @@ def delete_public(matching_zones):
msg = "There are multiple zones that match. Use hosted_zone_id to specify the correct zone."
else:
if not module.check_mode:
try:
client.delete_hosted_zone(Id=matching_zones[0]["Id"])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not get delete hosted zone {matching_zones[0]['Id']}")
delete_hosted_zone(matching_zones[0]["Id"])
changed = True
msg = f"Successfully deleted {matching_zones[0]['Id']}"
return changed, msg
Expand All @@ -443,18 +594,12 @@ def delete_hosted_id(hosted_zone_id, matching_zones):
for z in matching_zones:
deleted.append(z["Id"])
if not module.check_mode:
try:
client.delete_hosted_zone(Id=z["Id"])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not delete hosted zone {z['Id']}")
delete_hosted_zone(z["Id"])
changed = True
msg = f"Successfully deleted zones: {deleted}"
elif hosted_zone_id in [zo["Id"].replace("/hostedzone/", "") for zo in matching_zones]:
if not module.check_mode:
try:
client.delete_hosted_zone(Id=hosted_zone_id)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not delete hosted zone {hosted_zone_id}")
delete_hosted_zone(hosted_zone_id)
changed = True
msg = f"Successfully deleted zone: {hosted_zone_id}"
else:
Expand All @@ -463,6 +608,15 @@ def delete_hosted_id(hosted_zone_id, matching_zones):
return changed, msg


def delete_hosted_zone(hosted_zone_id):
try:
client.delete_hosted_zone(Id=hosted_zone_id)
except is_boto3_error_code("HostedZoneNotEmpty") as e:
module.fail_json_aws(e, msg=f"Could not get delete hosted zone {hosted_zone_id}")
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg=f"Could not delete hosted zone {hosted_zone_id}")


def delete(matching_zones):
zone_in = module.params.get("zone").lower()
vpc_id = module.params.get("vpc_id")
Expand Down Expand Up @@ -507,6 +661,7 @@ def main():
delegation_set_id=dict(),
tags=dict(type="dict", aliases=["resource_tags"]),
purge_tags=dict(type="bool", default=True),
dnssec=dict(type="bool", default=False),
)

mutually_exclusive = [
Expand Down
Loading
Loading