diff --git a/changelogs/fragments/869-aws_acm_certificate_requests.yml b/changelogs/fragments/869-aws_acm_certificate_requests.yml new file mode 100644 index 00000000000..81f6e70f6ed --- /dev/null +++ b/changelogs/fragments/869-aws_acm_certificate_requests.yml @@ -0,0 +1,2 @@ +minor_changes: +- aws_acm - Add ``wait``, ``wait_timeout`` parameters to wait until certificate operation completes in ACM. Add ``certificate_request`` and suboptions to submit certificate requests (https://github.com/ansible-collections/community.aws/pull/869). diff --git a/changelogs/fragments/870-aws_acm_certificate_tags.yml b/changelogs/fragments/870-aws_acm_certificate_tags.yml new file mode 100644 index 00000000000..cb44e26b3c1 --- /dev/null +++ b/changelogs/fragments/870-aws_acm_certificate_tags.yml @@ -0,0 +1,2 @@ +minor_changes: +- aws_acm - Add ``tags`` and ``purge_tags`` parameters to tag certificates in ACM (https://github.com/ansible-collections/community.aws/pull/870). diff --git a/plugins/modules/aws_acm.py b/plugins/modules/aws_acm.py index d28301e9160..9def436f8d4 100644 --- a/plugins/modules/aws_acm.py +++ b/plugins/modules/aws_acm.py @@ -29,15 +29,12 @@ --- module: aws_acm short_description: > - Upload and delete certificates in the AWS Certificate Manager service + Request, upload and delete certificates in the AWS Certificate Manager service. version_added: 1.0.0 description: - > - Import and delete certificates in Amazon Web Service's Certificate - Manager (AWS ACM). - - > - This module does not currently interact with AWS-provided certificates. - It currently only manages certificates provided to AWS by the user. + Request, renew, import and delete certificates in Amazon Web Service's + Certificate Manager (AWS ACM). - The ACM API allows users to upload multiple certificates for the same domain name, and even multiple identical certificates. This module attempts to restrict such freedoms, to be idempotent, as per the Ansible philosophy. @@ -79,15 +76,17 @@ certificate: description: - The body of the PEM encoded public certificate. - - Required when I(state) is not C(absent). + - Required when I(state) is not C(absent) and the certificate does not exist. - > If your certificate is in a file, use C(lookup('file', 'path/to/cert.pem')). type: str certificate_arn: description: - - The ARN of a certificate in ACM to delete - - Ignored when I(state=present). + - The ARN of a certificate in ACM to modify or delete. + - > + If I(state=present), the certificate with the specified ARN can be updated. + For example, this can be used to add/remove tags to an existing certificate. - > If I(state=absent), you must provide one of I(certificate_arn), I(domain_name) or I(name_tag). @@ -117,8 +116,23 @@ Exactly one of I(domain_name), I(name_tag) and I(certificate_arn) must be provided. - > - If I(state=present) this must not be specified. - (Since the domain name is encoded within the public certificate's body.) + If I(state=present) and I(certificate_request) is not specified, this must not be specified. + In that case, a certificate is imported to ACM; the domain name is encoded within + the public certificate's body. + - > + If I(state=present) and I(certificate_request) is specified, this must be specified. + A certificate is requested from ACM. In that case, the I(domain_name) is the fully + qualified domain name (FQDN), such as www.example.com, that you want to secure with + an ACM certificate. + - > + Use an asterisk (*) to create a wildcard certificate that protects several sites + in the same domain. + For example, *.example.com protects www.example.com, site.example.com + and images.example.com. + - > + The first domain name you enter cannot exceed 64 octets, including periods. + Each subsequent Subject Alternative Name (SAN), however, can be up to 253 octets + in length. type: str aliases: [domain] name_tag: @@ -131,6 +145,9 @@ This is to ensure Ansible can treat certificates idempotently, even though the ACM API allows duplicate certificates. - If I(state=preset), this must be specified. + - > + If I(state=absent) and I(name_tag) is specified, + this task will delete all ACM certificates with this Name tag. - > If I(state=absent), you must provide exactly one of I(certificate_arn), I(domain_name) or I(name_tag). @@ -139,12 +156,87 @@ private_key: description: - The body of the PEM encoded private key. - - Required when I(state=present). + - Required when I(state=present) and the certificate does not exist. - Ignored when I(state=absent). - > If your private key is in a file, use C(lookup('file', 'path/to/key.pem')). type: str + + certificate_request: + description: + - > + Requests an ACM certificate for use with other Amazon Web Services services. + To request an ACM certificate, you must specify a fully qualified domain name (FQDN) + in the I(domain_name) parameter. + You can also specify additional FQDNs in the I(subject_alternative_names) parameter. + - > + If you are requesting a private certificate, domain validation is not required. + - > + If you are requesting a public certificate, each domain name that you specify must + be validated to verify that you own or control the domain. + - > + You can use DNS validation or email validation. + ACM issues public certificates after receiving approval from the domain owner. + - > + At this time, only exported private certificates can be renewed. + version_added: 3.1.0 + suboptions: + subject_alternative_names: + description: + - > + Additional FQDNs to be included in the Subject Alternative Name extension of + the ACM certificate. + - > + For example, add the name www.example.net to a certificate for which the + I(domain_name) parameter is www.example.com if users can reach your site by + using either name. + type: list + elements: str + version_added: 3.1.0 + validation_method: + description: + - > + The method you want to use if you are requesting a public certificate to validate + that you own or control domain. + - > + You can validate with DNS or validate with email. + choices: ['DNS', 'EMAIL'] + type: str + version_added: 3.1.0 + certificate_authority_arn: + description: + - > + The Amazon Resource Name (ARN) of the private certificate authority (CA) that will + be used to issue the certificate. + - > + If you do not provide an ARN and you are trying to request a private certificate, + ACM will attempt to issue a public certificate. + type: str + version_added: 3.1.0 + options: + description: + - > + Currently, you can use this parameter to specify whether to add the certificate + to a certificate transparency log. + - > + Certificate transparency makes it possible to detect SSL/TLS certificates that + have been mistakenly or maliciously issued. Certificates that have not been logged + typically produce an error message in a browser. + version_added: 3.1.0 + suboptions: + certificate_transparency_logging_preference: + description: + - > + You can opt out of certificate transparency logging by specifying the DISABLED + option. Opt in by specifying ENABLED. + choices: ['ENABLED', 'DISABLED'] + type: str + default: 'ENABLED' + version_added: 3.1.0 + type: dict + type: dict + state: description: - > @@ -157,6 +249,45 @@ choices: [present, absent] default: present type: str + + tags: + description: + - Tags to apply to certificates imported in ACM. + - > + If both I(name_tag) and the 'Name' tag in I(tags) are set, + the values must be the same. + - > + If the 'Name' tag in I(tags) is not set and I(name_tag) is set, + the I(name_tag) value is copied to I(tags). + type: dict + version_added: 3.1.0 + + purge_tags: + description: + - whether to remove tags not present in the C(tags) parameter. + default: false + type: bool + version_added: 3.1.0 + + wait: + description: + - > + Whether or not to wait for the certificate operation to complete. + - > + When a certificate request is submitted, the certificate is created, + then the validation records. It may take some time for the validation + records to be generated. + type: bool + default: 'no' + version_added: 3.1.0 + + wait_timeout: + description: + - how long before wait gives up, in seconds. + default: 15 + type: int + version_added: 3.1.0 + author: - Matthew Davis (@matt-telstra) on behalf of Telstra Corporation Limited extends_documentation_fragment: @@ -206,6 +337,29 @@ state: absent region: ap-southeast-2 +- name: add tags to an existing certificate with a particular ARN + community.aws.aws_acm: + certificate_arn: "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901" + tags: + Name: my_certificate + Application: search + Environment: development + purge_tags: true + +- name: request a certificate issued by ACM + community.aws.aws_acm: + certificate_request: + domain_name: acm.ansible.com + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + tags: + Name: my_cert + Application: search + Environment: development ''' RETURN = ''' @@ -234,11 +388,79 @@ ''' +import base64 +from copy import deepcopy +import datetime +import random +import string +import re # regex library +import time + +try: + import botocore +except ImportError: + pass # handled by AnsibleAWSModule + from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.acm import ACMServiceManager +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ( + boto3_tag_list_to_ansible_dict, + ansible_dict_to_boto3_tag_list, +) from ansible.module_utils._text import to_text -import base64 -import re # regex library +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible.module_utils.six.moves import xrange + + +def ensure_tags(client, module, resource_arn, existing_tags, tags, purge_tags): + if tags is None: + return (False, existing_tags) + + tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, tags, purge_tags) + changed = bool(tags_to_add or tags_to_remove) + if tags_to_add: + if module.check_mode: + module.exit_json( + changed=True, msg="Would have added tags to domain if not in check mode" + ) + try: + client.add_tags_to_certificate( + CertificateArn=resource_arn, + Tags=ansible_dict_to_boto3_tag_list(tags_to_add), + ) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + module.fail_json_aws( + e, "Couldn't add tags to certificate {0}".format(resource_arn) + ) + if tags_to_remove: + if module.check_mode: + module.exit_json( + changed=True, msg="Would have removed tags if not in check mode" + ) + try: + # remove_tags_from_certificate wants a list of key, value pairs, not a list of keys. + tags_list = [{'Key': key, 'Value': existing_tags.get(key)} for key in tags_to_remove] + client.remove_tags_from_certificate( + CertificateArn=resource_arn, + Tags=tags_list, + ) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: + module.fail_json_aws( + e, "Couldn't remove tags from certificate {0}".format(resource_arn) + ) + new_tags = deepcopy(existing_tags) + for key, value in tags_to_add.items(): + new_tags[key] = value + for key in tags_to_remove: + new_tags.pop(key, None) + return (changed, new_tags) # Takes in two text arguments @@ -293,6 +515,292 @@ def pem_chain_split(module, pem): return pem_arr +def renew_certificate(client, module, acm, certificate, desired_tags): + """ + Renew an existing certificate in ACM. + """ + response = None + cert_arn = certificate['certificate_arn'] + if cert_arn is None: + module.fail_json(msg="Internal error. Certificate ARN not found", certificate=certificate) + # Rule to decide when to renew certificate. + + cert = acm.describe_certificate_with_backoff(client=client, certificate_arn=cert_arn) + # 'IMPORTED'|'AMAZON_ISSUED'|'PRIVATE' + cert_type = cert.get('Type') + cert_status = cert.get('Status') + eligible_for_renewal = False + send_new_certificate_request = False + if cert_type in ['AMAZON_ISSUED', 'PRIVATE']: + # Let AWS API do error handling of certificate renewal based on the certificate type. + # Look at the certificate status to decide whether to: + # 1) Do nothing. + # 2) Renew the certificate. + # 3) Send a new certificate request. + # 'PENDING_VALIDATION'|'ISSUED'|'INACTIVE'|'EXPIRED'|'VALIDATION_TIMED_OUT'|'REVOKED'|'FAILED' + if cert_status == 'ISSUED': + if cert.get('NotAfter') is None: + module.fail_json(msg="Internal error. Certificate 'NotAfter' date not found", certificate=cert) + # Do not attempt to renew the certificate indiscriminately. + # Obtain the account 'DaysBeforeExpiry' parameter to determine if the current date + # is close enough to the certificate expiration. + try: + response = client.get_account_configuration() + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "Couldn't renew certificate {0}".format(cert_arn)) + days_before_expiry = response['ExpiryEvents']['DaysBeforeExpiry'] + if datetime.now() > (cert.get('NotAfter') - datetime.timedelta(days_before_expiry)): + eligible_for_renewal = True + elif cert_status == 'PENDING_VALIDATION': + # Do nothing. The certificate cannot be renewed since it hasn't been validated yet. + return (False, cert_arn, response) + else: + # All other cases (inactive, expired, timeout...), send a new certificate request. + send_new_certificate_request = True + elif cert_type == 'IMPORTED': + module.fail_json(msg="Cannot renew imported certificate", certificate=cert) + else: + module.fail_json(msg="Unsupported certificate type", certificate=cert) + + if eligible_for_renewal: + if module.check_mode: + module.exit_json(changed=True, msg="Would have renewed certificate if not in check mode") + try: + response = client.renew_certificate(CertificateArn=cert_arn) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "Couldn't renew certificate {0}".format(cert_arn)) + return (True, cert_arn, response) + elif send_new_certificate_request: + return request_certificate(client, module, desired_tags) + + +def wait_for_validation_records(client, module, acm, cert_arn): + """ + Wait until the validation records of a certificate request are present. + When requesting a public certificate, it may take several seconds for the DNS|EMAIL validation records + to be generated. + """ + if not module.params.get('wait'): + return + timeout = module.params["wait_timeout"] + deadline = time.time() + timeout + while time.time() < deadline: + cert_data = acm.describe_certificate_with_backoff(client=client, certificate_arn=cert_arn) + cert_data = camel_dict_to_snake_dict(cert_data) + has_validation_records = True + if 'domain_validation_options' not in cert_data or len(cert_data['domain_validation_options']) == 0: + has_validation_records = False + else: + for dvo in cert_data['domain_validation_options']: + if dvo['validation_status'] == 'PENDING_VALIDATION' and 'resource_record' not in dvo: + has_validation_records = False + break + if has_validation_records: + return + time.sleep(5) + # Timeout occured + module.fail_json(msg="Timeout waiting for validation records") + + +def request_certificate(client, module, acm, desired_tags): + """ + Request a new certificate from ACM. + """ + absent_args = ['name_tag', 'domain_name'] + if sum([(module.params[a] is not None) for a in absent_args]) < 2: + module.fail_json(msg="When requesting a certificate, all of 'name_tag' and 'domain_name' must be specified") + cert_request = module.params.get('certificate_request') + ca_arn = cert_request.get('certificate_authority_arn') + domain_name = module.params.get('domain_name') + validation_method = cert_request.get('validation_method') + if ca_arn is None: + # Public certificate. Domain ownership validation is required. + if validation_method is None: + module.fail_json(msg="The 'validation_method' parameter must be specified when requesting a public certificate from ACM") + else: + # Private certificate. No domain ownership validation is required. + # Ignore the 'validation_method'. + validation_method = None + + cert_options = cert_request.get('options') + options = { + 'CertificateTransparencyLoggingPreference': 'ENABLED', + } + if cert_options is not None and cert_options.get('certificate_transparency_logging_preference') is not None: + options['CertificateTransparencyLoggingPreference'] = cert_options.get('certificate_transparency_logging_preference') + + response = None + changed = True + if module.check_mode: + module.exit_json( + changed=changed, msg="Would have requested certificate if not in check mode" + ) + idempotency_token = "".join([random.choice(string.ascii_letters) for i in xrange(16)]) + # The input 'desired_tags' argument is a dictionary, but ACM request_certificate wants + # a list of {Key, Value} pairs. + tags_list = [{'Key': key, 'Value': desired_tags.get(key)} for key in desired_tags] + parameters = { + 'DomainName': domain_name, + 'IdempotencyToken': idempotency_token, + 'Tags': tags_list, + } + if cert_request.get('validation_method') is not None: + parameters['ValidationMethod'] = cert_request.get('validation_method') + if cert_request.get('subject_alternative_names') is not None: + parameters['SubjectAlternativeNames'] = cert_request.get('subject_alternative_names') + if options is not None: + parameters['Options'] = options + if ca_arn is not None: + parameters['CertificateAuthorityArn'] = ca_arn + try: + response = client.request_certificate(**parameters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "Couldn't request certificate for {0}".format(domain_name)) + cert_arn = response.get('CertificateArn') + if ca_arn is None and module.params.get('wait'): + # Public certificate. Wait for the validation records to be present. + wait_for_validation_records(client, module, acm, cert_arn) + return (changed, cert_arn, response) + + +def update_imported_certificate(client, module, acm, old_cert, desired_tags): + """ + Update the existing certificate that was previously imported in ACM. + """ + module.debug("Existing certificate found in ACM") + if ('tags' not in old_cert) or ('Name' not in old_cert['tags']): + # shouldn't happen + module.fail_json(msg="Internal error, unsure which certificate to update", certificate=old_cert) + if module.params.get('name_tag') is not None and (old_cert['tags']['Name'] != module.params.get('name_tag')): + # This could happen if the user identified the certificate using 'certificate_arn' or 'domain_name', + # and the 'Name' tag in the AWS API does not match the ansible 'name_tag'. + module.fail_json(msg="Internal error, Name tag does not match", certificate=old_cert) + if 'certificate' not in old_cert: + # shouldn't happen + module.fail_json(msg="Internal error, unsure what the existing cert in ACM is", certificate=old_cert) + + cert_arn = None + # Are the existing certificate in ACM and the local certificate the same? + same = True + if module.params.get('certificate') is not None: + same &= chain_compare(module, old_cert['certificate'], module.params['certificate']) + if module.params['certificate_chain']: + # Need to test this + # not sure if Amazon appends the cert itself to the chain when self-signed + same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate_chain']) + else: + # When there is no chain with a cert + # it seems Amazon returns the cert itself as the chain + same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate']) + + if same: + module.debug("Existing certificate in ACM is the same") + cert_arn = old_cert['certificate_arn'] + changed = False + else: + absent_args = ['certificate', 'name_tag', 'private_key'] + if sum([(module.params[a] is not None) for a in absent_args]) < 3: + module.fail_json(msg="When importing a certificate, all of 'name_tag', 'certificate' and 'private_key' must be specified") + module.debug("Existing certificate in ACM is different, overwriting") + changed = True + if module.check_mode: + cert_arn = old_cert['certificate_arn'] + # note: returned domain will be the domain of the previous cert + else: + # update cert in ACM + cert_arn = acm.import_certificate( + client, + module, + certificate=module.params['certificate'], + private_key=module.params['private_key'], + certificate_chain=module.params['certificate_chain'], + arn=old_cert['certificate_arn'], + tags=desired_tags, + ) + return (changed, cert_arn, cert_arn) + + +def import_certificate(client, module, acm, desired_tags): + """ + Import a certificate to ACM. + """ + # Validate argument requirements + absent_args = ['certificate', 'name_tag', 'private_key'] + cert_arn = None + if sum([(module.params[a] is not None) for a in absent_args]) < 3: + module.fail_json(msg="When importing a new certificate, all of 'name_tag', 'certificate' and 'private_key' must be specified") + module.debug("No certificate in ACM. Creating new one.") + changed = True + if module.check_mode: + domain = 'example.com' + module.exit_json(certificate=dict(domain_name=domain), changed=True) + else: + cert_arn = acm.import_certificate( + client, + module, + certificate=module.params['certificate'], + private_key=module.params['private_key'], + certificate_chain=module.params['certificate_chain'], + tags=desired_tags, + ) + return (changed, cert_arn, cert_arn) + + +def ensure_certificates_present(client, module, acm, certificates, desired_tags, filter_tags): + cert_arn = None + changed = False + response = None + if len(certificates) > 1: + msg = "More than one certificate with Name=%s exists in ACM in this region" % module.params['name_tag'] + module.fail_json(msg=msg, certificates=certificates) + elif len(certificates) == 1: + if module.params.get('certificate_request') is not None: + # Renew existing certificate requested from ACM + (changed, cert_arn, response) = renew_certificate(client, module, acm, certificates[0], desired_tags) + else: + # Update existing certificate that was previously imported to ACM. + (changed, cert_arn, response) = update_imported_certificate(client, module, acm, certificates[0], desired_tags) + else: # len(certificates) == 0 + if module.params.get('certificate_request') is not None: + # Request certificate from ACM. + (changed, cert_arn, response) = request_certificate(client, module, acm, desired_tags) + else: + # Import new certificate to ACM. + (changed, cert_arn, response) = import_certificate(client, module, acm, desired_tags) + + if cert_arn is None: + module.fail_json(msg="Internal error. Could not identify certificate ARN") + + # Add/remove tags to/from certificate + try: + existing_tags = boto3_tag_list_to_ansible_dict(client.list_tags_for_certificate(CertificateArn=cert_arn)['Tags']) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, "Couldn't get tags for certificate") + + purge_tags = module.params.get('purge_tags') + (c, new_tags) = ensure_tags(client, module, cert_arn, existing_tags, desired_tags, purge_tags) + changed |= c + + cert_data = acm.describe_certificate_with_backoff(client=client, certificate_arn=cert_arn) + cert_data = camel_dict_to_snake_dict(cert_data) + # The dict already contains a 'certificate_arn' attribute which is the same value as 'arn'. + # This 'aws_acm' module was originally written to return the 'arn' attribute. + cert_data['arn'] = cert_arn + cert_data['tags'] = new_tags + if 'domain_name' not in cert_data: + # The 'DomainName' attribute may not be present when describing a certificate issued by AWS. + cert_data['domain_name'] = module.params.get("domain_name") + + module.exit_json(certificate=cert_data, changed=changed) + + +def ensure_certificates_absent(client, module, acm, certificates): + for cert in certificates: + if not module.check_mode: + acm.delete_certificate(client, module, cert['certificate_arn']) + module.exit_json(arns=[cert['certificate_arn'] for cert in certificates], changed=(len(certificates) > 0)) + + def main(): argument_spec = dict( certificate=dict(), @@ -301,112 +809,89 @@ def main(): domain_name=dict(aliases=['domain']), name_tag=dict(aliases=['name']), private_key=dict(no_log=True), - state=dict(default='present', choices=['present', 'absent']) + certificate_request=dict( + type="dict", + default=None, + options=dict( + subject_alternative_names=dict(type="list", elements="str"), + validation_method=dict(default=None, choices=['DNS', 'EMAIL']), + options=dict( + type="dict", + default=None, + options=dict( + certificate_transparency_logging_preference=dict(choices=['ENABLED', 'DISABLED'], default='ENABLED'), + ) + ), + certificate_authority_arn=dict(), + ), + ), + tags=dict(type='dict'), + purge_tags=dict(type='bool', default=False), + wait=dict(type="bool", default=False), + wait_timeout=dict(type="int", default=15), + state=dict(default='present', choices=['present', 'absent']), + ) + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['certificate_request', 'private_key'], + ['certificate_request', 'certificate_chain'], + ], ) - required_if = [ - ['state', 'present', ['certificate', 'name_tag', 'private_key']], - ] - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) acm = ACMServiceManager(module) # Check argument requirements if module.params['state'] == 'present': - if module.params['certificate_arn']: - module.fail_json(msg="Parameter 'certificate_arn' is only valid if parameter 'state' is specified as 'absent'") + # at least one of these should be specified. + absent_args = ['certificate_arn', 'domain_name', 'name_tag'] + if sum([(module.params[a] is not None) for a in absent_args]) < 1: + for a in absent_args: + module.debug("%s is %s" % (a, module.params[a])) + module.fail_json(msg="If 'state' is specified as 'present' then at least one of 'name_tag', 'certificate_arn' or 'domain_name' must be specified") else: # absent # exactly one of these should be specified absent_args = ['certificate_arn', 'domain_name', 'name_tag'] if sum([(module.params[a] is not None) for a in absent_args]) != 1: for a in absent_args: module.debug("%s is %s" % (a, module.params[a])) - module.fail_json(msg="If 'state' is specified as 'absent' then exactly one of 'name_tag', certificate_arn' or 'domain_name' must be specified") - - if module.params['name_tag']: - tags = dict(Name=module.params['name_tag']) - else: - tags = None + module.fail_json(msg="If 'state' is specified as 'absent' then exactly one of 'name_tag', 'certificate_arn' or 'domain_name' must be specified") + + filter_tags = None + desired_tags = None + if module.params.get('tags') is not None: + desired_tags = module.params['tags'] + if module.params.get('name_tag') is not None: + # The module was originally implemented to filter certificates based on the 'Name' tag. + # Other tags are not used to filter certificates. + # It would make sense to replace the existing name_tag, domain, certificate_arn attributes + # with a 'filter' attribute, but that would break backwards-compatibility. + filter_tags = dict(Name=module.params['name_tag']) + if desired_tags is not None: + if 'Name' in desired_tags: + if desired_tags['Name'] != module.params['name_tag']: + module.fail_json(msg="Value of 'name_tag' conflicts with value of 'tags.Name'") + else: + desired_tags['Name'] = module.params['name_tag'] + else: + desired_tags = deepcopy(filter_tags) client = module.client('acm') # fetch the list of certificates currently in ACM - certificates = acm.get_certificates(client=client, - module=module, - domain_name=module.params['domain_name'], - arn=module.params['certificate_arn'], - only_tags=tags) + certificates = acm.get_certificates( + client=client, + module=module, + domain_name=module.params['domain_name'], + arn=module.params['certificate_arn'], + only_tags=filter_tags, + ) module.debug("Found %d corresponding certificates in ACM" % len(certificates)) - if module.params['state'] == 'present': - if len(certificates) > 1: - msg = "More than one certificate with Name=%s exists in ACM in this region" % module.params['name_tag'] - module.fail_json(msg=msg, certificates=certificates) - elif len(certificates) == 1: - # update the existing certificate - module.debug("Existing certificate found in ACM") - old_cert = certificates[0] # existing cert in ACM - if ('tags' not in old_cert) or ('Name' not in old_cert['tags']) or (old_cert['tags']['Name'] != module.params['name_tag']): - # shouldn't happen - module.fail_json(msg="Internal error, unsure which certificate to update", certificate=old_cert) - - if 'certificate' not in old_cert: - # shouldn't happen - module.fail_json(msg="Internal error, unsure what the existing cert in ACM is", certificate=old_cert) - - # Are the existing certificate in ACM and the local certificate the same? - same = True - same &= chain_compare(module, old_cert['certificate'], module.params['certificate']) - if module.params['certificate_chain']: - # Need to test this - # not sure if Amazon appends the cert itself to the chain when self-signed - same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate_chain']) - else: - # When there is no chain with a cert - # it seems Amazon returns the cert itself as the chain - same &= chain_compare(module, old_cert['certificate_chain'], module.params['certificate']) - - if same: - module.debug("Existing certificate in ACM is the same, doing nothing") - domain = acm.get_domain_of_cert(client=client, module=module, arn=old_cert['certificate_arn']) - module.exit_json(certificate=dict(domain_name=domain, arn=old_cert['certificate_arn']), changed=False) - else: - module.debug("Existing certificate in ACM is different, overwriting") - - if module.check_mode: - arn = old_cert['certificate_arn'] - # note: returned domain will be the domain of the previous cert - else: - # update cert in ACM - arn = acm.import_certificate(client, module, - certificate=module.params['certificate'], - private_key=module.params['private_key'], - certificate_chain=module.params['certificate_chain'], - arn=old_cert['certificate_arn'], - tags=tags) - domain = acm.get_domain_of_cert(client=client, module=module, arn=arn) - module.exit_json(certificate=dict(domain_name=domain, arn=arn), changed=True) - else: # len(certificates) == 0 - module.debug("No certificate in ACM. Creating new one.") - if module.check_mode: - domain = 'example.com' - module.exit_json(certificate=dict(domain_name=domain), changed=True) - else: - arn = acm.import_certificate(client=client, - module=module, - certificate=module.params['certificate'], - private_key=module.params['private_key'], - certificate_chain=module.params['certificate_chain'], - tags=tags) - domain = acm.get_domain_of_cert(client=client, module=module, arn=arn) - - module.exit_json(certificate=dict(domain_name=domain, arn=arn), changed=True) - + ensure_certificates_present(client, module, acm, certificates, desired_tags, filter_tags) else: # state == absent - for cert in certificates: - if not module.check_mode: - acm.delete_certificate(client, module, cert['certificate_arn']) - module.exit_json(arns=[cert['certificate_arn'] for cert in certificates], - changed=(len(certificates) > 0)) + ensure_certificates_absent(client, module, acm, certificates) if __name__ == '__main__': diff --git a/tests/integration/targets/aws_acm/aliases b/tests/integration/targets/aws_acm/aliases index 6977920fdc5..359ddb286c4 100644 --- a/tests/integration/targets/aws_acm/aliases +++ b/tests/integration/targets/aws_acm/aliases @@ -1,5 +1,5 @@ # https://github.com/ansible/ansible/issues/67788 -unstable +# unstable cloud/aws diff --git a/tests/integration/targets/aws_acm/tasks/cert_requests.yml b/tests/integration/targets/aws_acm/tasks/cert_requests.yml new file mode 100644 index 00000000000..699056a71d1 --- /dev/null +++ b/tests/integration/targets/aws_acm/tasks/cert_requests.yml @@ -0,0 +1,261 @@ +- name: AWS ACM integration test, request certificates + block: + - set_fact: + name_tag_public_cert_request: "public cert for acm.ansible.com" + name_tag_private_cert_request: "private cert for acm.ansible.com" + - name: delete certificate requests + # Some of the requests may pre-exists from previous test execution. + community.aws.aws_acm: + name_tag: "{{ name_tag_public_cert_request }}" + state: absent + register: result + - name: Missing 'domain_mame' parameter + community.aws.aws_acm: + certificate_request: null + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"If ''state'' is specified as ''present'' then at least one of ''name_tag'', ''certificate_arn'' or ''domain_name'' must be specified" in result.msg' + - name: "'certificate_request' and 'private_key' are mutually exclusive" + community.aws.aws_acm: + domain_name: acm.ansible.com + certificate_request: null + private_key: "abc" + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"parameters are mutually exclusive: certificate_request|private_key" in result.msg' + - name: "'certificate_request' and 'certificate_chain' are mutually exclusive" + community.aws.aws_acm: + domain_name: acm.ansible.com + certificate_request: null + certificate_chain: "abc" + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"parameters are mutually exclusive: certificate_request|certificate_chain" in result.msg' + - name: request a public certificate, but name_tag is missing + community.aws.aws_acm: + domain_name: acm.ansible.com + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"When requesting a certificate, all of ''name_tag'' and ''domain_name'' must be specified" in result.msg' + - name: request a public certificate, but validation method is missing + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + options: + certificate_transparency_logging_preference: ENABLED + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"The ''validation_method'' parameter must be specified when requesting a public certificate from ACM" in result.msg' + - name: request a public certificate, but value of validation method is wrong + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: SMS # unsupported value. Must be DNS or EMAIL + options: + certificate_transparency_logging_preference: ENABLED + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"value of validation_method must be one of: DNS, EMAIL, got: SMS found in certificate_request" in result.msg' + - name: request a public certificate, but certificate_transparency_logging_preference is wrong + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + # Wrong value, must be ENABLED or DISABLED + certificate_transparency_logging_preference: GARBAGE + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"value of certificate_transparency_logging_preference must be one of: ENABLED, DISABLED, got: GARBAGE found in certificate_request" in result.msg' + - name: request a public certificate, check mode + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + register: result + check_mode: true + - assert: + that: + - result.changed + - name: request a public certificate, expect changes + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + # Don't use EMAIL validation, because: + # 1) AWS recommends DNS validation. + # 2) Email validation would be harder to automate in integration tests. + # 3) AWS would actually send email to webmaster@acm.ansible.com. + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + tags: + Application: search + Environment: development + # Wait until the validation resource records have been created. + wait: true + register: result + - assert: + that: + - result.certificate.arn is defined + - result.certificate.domain_name is defined + - result.certificate.domain_name == 'acm.ansible.com' + - result.certificate.tags is defined + - result.certificate.tags | length == 3 + - result.certificate.domain_name == 'acm.ansible.com' + - result.changed + - set_fact: + public_cert_request_arn: "{{ result.certificate.arn }}" + - name: get the certificate request that was just created + aws_acm_info: + certificate_arn: "{{ public_cert_request_arn }}" + register: result + - assert: + that: + - result.certificates | length == 1 + - result.certificates[0].certificate_arn is defined + - result.certificates[0].domain_name is defined + - result.certificates[0].subject_alternative_names is defined + - result.certificates[0].subject_alternative_names | length == 3 + - result.certificates[0].issuer is defined + - "result.certificates[0].certificate_arn == '{{ public_cert_request_arn }}'" + - "result.certificates[0].domain_name == 'acm.ansible.com'" + - "result.certificates[0].issuer == 'Amazon'" + - "result.certificates[0].domain_validation_options is defined" + - "result.certificates[0].domain_validation_options | length == 3" + - "result.certificates[0].domain_validation_options[0].validation_method == 'DNS'" + - "result.certificates[0].domain_validation_options[1].validation_method == 'DNS'" + - "result.certificates[0].domain_validation_options[2].validation_method == 'DNS'" + - "result.certificates[0].domain_validation_options[0].domain_name == 'acm.ansible.com'" + - "result.certificates[0].domain_validation_options[1].domain_name == 'acm-east.ansible.com'" + - "result.certificates[0].domain_validation_options[2].domain_name == 'acm-west.ansible.com'" + - "result.certificates[0].domain_validation_options[0].validation_status == 'PENDING_VALIDATION'" + - "result.certificates[0].domain_validation_options[1].validation_status == 'PENDING_VALIDATION'" + - "result.certificates[0].domain_validation_options[2].validation_status == 'PENDING_VALIDATION'" + # Note: it takes time for the validation records to be generated. + # After a certificate request has been submmitted, the AWS API responds quickly, + # however the validation records are not generated yet. It may take a few seconds for the AWS + # backend to generate the resource records. + # This is why above the 'wait: true' attribute has been set, such that we can reliably get + # to this point and assert the resource records exist. + - "result.certificates[0].domain_validation_options[0].resource_record is defined" + - "result.certificates[0].domain_validation_options[1].resource_record is defined" + - "result.certificates[0].domain_validation_options[2].resource_record is defined" + - "result.certificates[0].domain_validation_options[0].resource_record.name is defined" + - "result.certificates[0].domain_validation_options[1].resource_record.name is defined" + - "result.certificates[0].domain_validation_options[2].resource_record.name is defined" + - "result.certificates[0].domain_validation_options[0].resource_record.value is defined" + - "result.certificates[0].domain_validation_options[1].resource_record.value is defined" + - "result.certificates[0].domain_validation_options[2].resource_record.value is defined" + + - name: request a public certificate, check mode again + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + register: result + check_mode: true + - assert: + that: + - not result.changed + - name: request a public certificate, no change expected + community.aws.aws_acm: + domain_name: acm.ansible.com + name_tag: "{{ name_tag_public_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-east.ansible.com + - acm-west.ansible.com + validation_method: DNS + options: + certificate_transparency_logging_preference: ENABLED + register: result + - assert: + that: + - not result.changed + + - name: delete certificate request for public certificate + community.aws.aws_acm: + name_tag: "{{ name_tag_public_cert_request }}" + state: absent + register: result + + - name: request a private certificate, wrong CA arn + community.aws.aws_acm: + domain_name: acm-private.ansible.com + name_tag: "{{ name_tag_private_cert_request }}" + certificate_request: + subject_alternative_names: + - acm-private-east.ansible.com + - acm-private-west.ansible.com + certificate_authority_arn: arn:aws:acm-pca:us-east-1:wrong + tags: + Application: search + Environment: development + register: result + ignore_errors: true + - assert: + that: + - 'result.failed' + - '"Value ''arn:aws:acm-pca:us-east-1:wrong'' at ''certificateAuthorityArn'' failed to satisfy constraint" in result.msg' + + # - name: delete certificate request for private certificate + # community.aws.aws_acm: + # name_tag: "{{ name_tag_private_cert_request }}" + # state: absent + # register: result diff --git a/tests/integration/targets/aws_acm/tasks/full_acm_test.yml b/tests/integration/targets/aws_acm/tasks/full_acm_test.yml index c3ec002e79b..a02b07b3a37 100644 --- a/tests/integration/targets/aws_acm/tasks/full_acm_test.yml +++ b/tests/integration/targets/aws_acm/tasks/full_acm_test.yml @@ -68,13 +68,12 @@ common_name: '{{ item.domain }}' - name: Generate a Self Signed OpenSSL certificate for own certs with_items: '{{ local_certs }}' - community.crypto.openssl_certificate: + community.crypto.x509_certificate: provider: selfsigned path: '{{ item.cert }}' csr_path: '{{ item.csr }}' privatekey_path: '{{ item.priv_key }}' - signature_algorithms: - - sha256WithRSAEncryption + selfsigned_digest: sha256 - name: upload certificate with check mode aws_acm: name_tag: '{{ item.name }}' @@ -392,14 +391,13 @@ common_name: '{{ chained_cert.domain }}' - name: Sign new certs with cert 0 and 1 with_items: '{{ chained_cert.chains }}' - community.crypto.openssl_certificate: + community.crypto.x509_certificate: provider: ownca path: '{{ item.cert }}' csr_path: '{{ item.csr }}' ownca_path: '{{ local_certs[item.ca].cert }}' ownca_privatekey_path: '{{ local_certs[item.ca].priv_key }}' - signature_algorithms: - - sha256WithRSAEncryption + selfsigned_digest: sha256 - name: check files exist (for next task) file: path: '{{ item }}' diff --git a/tests/integration/targets/aws_acm/tasks/main.yml b/tests/integration/targets/aws_acm/tasks/main.yml index 7565f4bb495..4e696123b32 100644 --- a/tests/integration/targets/aws_acm/tasks/main.yml +++ b/tests/integration/targets/aws_acm/tasks/main.yml @@ -12,6 +12,19 @@ - set_fact: aws_acm_test_uuid: "{{ (10**9) | random }}" + # Integration tests for certificate requests + - include_tasks: cert_requests.yml + + - name: attempt to delete cert without specifying required parameter + aws_acm: + state: absent + register: result + ignore_errors: true + - name: assert failure when name_tag conflicts with tags.Name + assert: + that: + - 'result.failed' + - '"If ''state'' is specified as ''absent'' then exactly one of ''name_tag''" in result.msg' - name: list certs aws_acm_info: null register: list_all @@ -53,19 +66,42 @@ common_name: '{{ item.domain }}' - name: Generate a Self Signed OpenSSL certificate for own certs with_items: '{{ local_certs }}' - community.crypto.openssl_certificate: + community.crypto.x509_certificate: provider: selfsigned path: '{{ item.cert }}' csr_path: '{{ item.csr }}' privatekey_path: '{{ item.priv_key }}' - signature_algorithms: - - sha256WithRSAEncryption + selfsigned_digest: sha256 + - name: try to upload certificate, but name_tag conflicts with tags.Name + vars: + local_cert: '{{ local_certs[0] }}' + aws_acm: + name_tag: '{{ local_cert.name }}' + certificate: '{{ lookup(''file'', local_cert.cert ) }}' + private_key: '{{ lookup(''file'', local_cert.priv_key ) }}' + state: present + tags: + Name: '{{ local_cert.name }}-other' + Application: search + Environment: development + register: result + ignore_errors: true + - name: assert failure when name_tag conflicts with tags.Name + assert: + that: + - 'result.failed' + - '"conflicts with value of" in result.msg' - name: upload certificates first time aws_acm: name_tag: '{{ item.name }}' certificate: '{{ lookup(''file'', item.cert ) }}' private_key: '{{ lookup(''file'', item.priv_key ) }}' state: present + # Add tags + tags: + Application: search + Environment: development + purge_tags: false register: upload with_items: '{{ local_certs }}' until: upload is succeeded @@ -95,7 +131,11 @@ - fetch_after_up_result.certificates[0].domain_name == original_cert.domain - (fetch_after_up_result.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', '')) == (lookup( 'file', original_cert.cert ) | replace( ' ', '' ) | replace( '\n', '' )) - '''Name'' in fetch_after_up_result.certificates[0].tags' + - '''Application'' in fetch_after_up_result.certificates[0].tags' + - '''Environment'' in fetch_after_up_result.certificates[0].tags' - fetch_after_up_result.certificates[0].tags['Name'] == original_cert.name + - fetch_after_up_result.certificates[0].tags['Application'] == 'search' + - fetch_after_up_result.certificates[0].tags['Environment'] == 'development' with_items: '{{ fetch_after_up.results }}' vars: fetch_after_up_result: '{{ item }}' @@ -154,6 +194,143 @@ register: upload2 with_items: '{{ local_certs }}' failed_when: upload2.changed + - name: change tags of existing certificate, check mode + aws_acm: + certificate_arn: '{{ certificate_arn }}' + tags: + Name: '{{ name_tag }}' + Application: search + Environment: staging + Owner: Bob + register: certificate_with_tags + check_mode: true + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + domain_name: '{{ upload2.results[0].certificate.domain_name }}' + - assert: + that: + - certificate_with_tags.changed + - name: change tags of existing certificate, changes expected + aws_acm: + # When applying tags to an existing certificate, it is sufficient to specify the 'certificate_arn'. + # Previously, the 'aws_acm' module was requiring the 'certificate', 'name_tag' and 'domain_name' + # attributes. + certificate_arn: '{{ certificate_arn }}' + tags: + Name: '{{ name_tag }}' + Application: search + Environment: staging + Owner: Bob + register: certificate_with_tags + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + domain_name: '{{ upload2.results[0].certificate.domain_name }}' + - name: assert certificate tags + assert: + that: + - certificate_with_tags.certificate.tags | length == 4 + - '''Name'' in certificate_with_tags.certificate.tags' + - '''Application'' in certificate_with_tags.certificate.tags' + - '''Environment'' in certificate_with_tags.certificate.tags' + - '''Owner'' in certificate_with_tags.certificate.tags' + - certificate_with_tags.certificate.tags['Name'] == name_tag + - certificate_with_tags.certificate.tags['Application'] == 'search' + - certificate_with_tags.certificate.tags['Environment'] == 'staging' + - certificate_with_tags.certificate.tags['Owner'] == 'Bob' + - certificate_with_tags.changed + vars: + name_tag: '{{ upload2.results[0].item.name }}' + - name: change tags of existing certificate, check mode again + aws_acm: + certificate_arn: '{{ certificate_arn }}' + tags: + Name: '{{ name_tag }}' + Application: search + Environment: staging + Owner: Bob + register: certificate_with_tags + check_mode: true + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + - assert: + that: + - not certificate_with_tags.changed + - name: change tags of existing certificate, no change expected + aws_acm: + certificate_arn: '{{ certificate_arn }}' + tags: + Name: '{{ name_tag }}' + Application: search + Environment: staging + Owner: Bob + register: certificate_with_tags + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + - name: assert certificate tags + assert: + that: + - certificate_with_tags.certificate.tags | length == 4 + - '''Name'' in certificate_with_tags.certificate.tags' + - '''Application'' in certificate_with_tags.certificate.tags' + - '''Environment'' in certificate_with_tags.certificate.tags' + - '''Owner'' in certificate_with_tags.certificate.tags' + - certificate_with_tags.certificate.tags['Name'] == name_tag + - certificate_with_tags.certificate.tags['Application'] == 'search' + - certificate_with_tags.certificate.tags['Environment'] == 'staging' + - certificate_with_tags.certificate.tags['Owner'] == 'Bob' + - not certificate_with_tags.changed + vars: + name_tag: '{{ upload2.results[0].item.name }}' + - name: check fetched data of cert we just uploaded + vars: + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + domain_name: '{{ upload2.results[0].certificate.domain_name }}' + name_tag: '{{ upload2.results[0].item.name }}' + assert: + that: + - certificate_with_tags.certificate.arn == certificate_arn + - certificate_with_tags.certificate.tags | length == 4 + - '''Name'' in certificate_with_tags.certificate.tags' + - '''Application'' in certificate_with_tags.certificate.tags' + - '''Environment'' in certificate_with_tags.certificate.tags' + - '''Owner'' in certificate_with_tags.certificate.tags' + - certificate_with_tags.certificate.tags['Name'] == name_tag + - certificate_with_tags.certificate.tags['Application'] == 'search' + - certificate_with_tags.certificate.tags['Environment'] == 'staging' + - certificate_with_tags.certificate.tags['Owner'] == 'Bob' + - name: change tags of existing certificate, purge tags + aws_acm: + certificate_arn: '{{ certificate_arn }}' + tags: + Name: '{{ name_tag }}' + Application: search + Environment: staging + # 'Owner' tag should be removed because 'purge_tags: true' + purge_tags: true + register: certificate_with_tags + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + domain_name: '{{ upload2.results[0].certificate.domain_name }}' + - name: check fetched data of cert we just uploaded + vars: + name_tag: '{{ upload2.results[0].item.name }}' + certificate_arn: '{{ upload2.results[0].certificate.arn }}' + domain_name: '{{ upload2.results[0].certificate.domain_name }}' + assert: + that: + - certificate_with_tags.certificate.arn == certificate_arn + - certificate_with_tags.certificate.tags | length == 3 + - '''Name'' in certificate_with_tags.certificate.tags' + - '''Application'' in certificate_with_tags.certificate.tags' + - '''Environment'' in certificate_with_tags.certificate.tags' + - certificate_with_tags.certificate.tags['Name'] == name_tag + - certificate_with_tags.certificate.tags['Application'] == 'search' + - certificate_with_tags.certificate.tags['Environment'] == 'staging' - name: update first cert with body of the second, first time aws_acm: state: present @@ -293,14 +470,13 @@ common_name: '{{ chained_cert.domain }}' - name: Sign new certs with cert 0 and 1 with_items: '{{ chained_cert.chains }}' - community.crypto.openssl_certificate: + community.crypto.x509_certificate: provider: ownca path: '{{ item.cert }}' csr_path: '{{ item.csr }}' ownca_path: '{{ local_certs[item.ca].cert }}' ownca_privatekey_path: '{{ local_certs[item.ca].priv_key }}' - signature_algorithms: - - sha256WithRSAEncryption + selfsigned_digest: sha256 - name: check files exist (for next task) file: path: '{{ item }}'