summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Davis <Matthew.Davis.2@team.telstra.com>2019-11-05 19:57:08 +0100
committerJill R <4121322+jillr@users.noreply.github.com>2019-11-05 19:57:08 +0100
commit4ee9f40e6230bf9068ff3d6fad24de71929ad458 (patch)
treef1160f3ce860fc3c00fdf1c1101ce38c72517443
parentRemove empty overridden unittest.setUp and unittest.tearDown methods. (diff)
downloadansible-4ee9f40e6230bf9068ff3d6fad24de71929ad458.tar.xz
ansible-4ee9f40e6230bf9068ff3d6fad24de71929ad458.zip
Add aws_acm module (#60552)
* convert aws_acm_facts to AnsibleAWSModule * factor aws_acm_facts into module_utils * add more filtering options for aws_acm_info * add aws_acm module and tests * uncomment aws_acm test * fix linting for aws_acm * fix __future__ linting for aws_acm * fix linting for aws_acm * fix linting for aws_acm * fix linting for aws_acm * fix linting for aws_acm * fix aws_acm_info arg type * remove test for old module name aws_acm_facts * simplify AWS ACM client creation * fix indent typo in aws_acm test * catch BotoCoreError in aws_acm * fix indent typo in aws_acm test * tighten AWS ACM test policy resource * move aws acm int test to venv * remove errant file * fix AWS ACM int test perms * undo copyright addition to wrong file * fix invalid log message in aws_acm Co-Authored-By: Jill R <4121322+jillr@users.noreply.github.com> * rephrase aws_acm_info doc from facts to information Co-Authored-By: Jill R <4121322+jillr@users.noreply.github.com> * rename aws_facts var to aws_info * remove case insensitivity for aws_acm pem compare * add no_log for aws_acm credential setting * add per-test prefix to aws_acm test resource names * make aws_acm use crypto module_util * clarify copyright for aws_acm * make aws_acm int test clearer * add explicit crypto dependency to aws_acm * change requests for aws_acm pr * fix wrong copyright owner aws_acm test * fix wrong copyright owner aws_acm test * rewrite aws_acm cert chain compare with regex, no dependency * fix linting for aws_acm unit test * fix linting for aws_acm unit test * fix linting and duplicate ignore * fix failed cert chain split in aws_acm, add more tests * remove errant file * more linting fixes for aws_acm * fix sanity ignore * rewrite cert compare in aws_acm to use base64 decode * improve regex for pem cert chain split in aws_acm * undo changes to crypto module util for aws_acm * increment ansible version for new aws_acm module * convert aws_acm return(x) to return x * increment version added for aws_acm_info new features * fix linting * fix bugs with AWS ACM * fix bad rebase * disable AWS ACM integration test, due to AWS account limit issue * remove aws acm integration test from shippable group
-rw-r--r--hacking/aws_config/testing_policies/security-policy.json25
-rw-r--r--lib/ansible/module_utils/aws/acm.py213
-rw-r--r--lib/ansible/modules/cloud/amazon/aws_acm.py403
-rw-r--r--lib/ansible/modules/cloud/amazon/aws_acm_info.py135
-rw-r--r--test/integration/targets/aws_acm/aliases3
-rw-r--r--test/integration/targets/aws_acm/defaults/main.yml40
-rw-r--r--test/integration/targets/aws_acm/meta/main.yml2
-rw-r--r--test/integration/targets/aws_acm/tasks/full_acm_test.yml504
-rw-r--r--test/integration/targets/aws_acm/tasks/main.yml33
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/a.pem31
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/b.pem47
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-1.0.cert121
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-1.1.cert69
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-1.2.cert113
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-1.3.cert124
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-1.4.cert86
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/chain-4.cert121
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/simple-chain-a.cert18
-rw-r--r--test/units/modules/cloud/amazon/fixtures/certs/simple-chain-b.cert18
-rw-r--r--test/units/modules/cloud/amazon/test_aws_acm.py122
20 files changed, 2140 insertions, 88 deletions
diff --git a/hacking/aws_config/testing_policies/security-policy.json b/hacking/aws_config/testing_policies/security-policy.json
index e38842ef55..f0d0768a7a 100644
--- a/hacking/aws_config/testing_policies/security-policy.json
+++ b/hacking/aws_config/testing_policies/security-policy.json
@@ -195,6 +195,31 @@
"arn:aws:iam::{{ aws_account }}:user/ansible-test*",
"arn:aws:iam::{{ aws_account }}:group/ansible-test*"
]
+ },
+ {
+ "Sid": "AllowAccessToACMRestrictable",
+ "Effect": "Allow",
+ "Action": [
+ "acm:ImportCertificate",
+ "acm:DescribeCertificate",
+ "acm:GetCertificate",
+ "acm:AddTagsToCertificate",
+ "acm:DeleteCertificate"
+ ],
+ "Resource": [
+ "arn:aws:acm:{{aws_region}}:{{aws_account}}:certificate/*"
+ ]
+ },
+ {
+ "Sid": "AllowAccessToACMUnrestrictable",
+ "Effect": "Allow",
+ "Action": [
+ "acm:ListCertificates",
+ "acm:ListTagsForCertificate"
+ ],
+ "Resource": [
+ "*"
+ ]
}
]
}
diff --git a/lib/ansible/module_utils/aws/acm.py b/lib/ansible/module_utils/aws/acm.py
new file mode 100644
index 0000000000..ed38027998
--- /dev/null
+++ b/lib/ansible/module_utils/aws/acm.py
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# - Matthew Davis <Matthew.Davis.2@team.telstra.com>
+# on behalf of Telstra Corporation Limited
+#
+# Common functionality to be used by the modules:
+# - acm
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+"""
+Common Amazon Certificate Manager facts shared between modules
+"""
+import traceback
+from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn
+from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, HAS_BOTO3, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
+from ansible.module_utils._text import to_bytes
+
+
+try:
+ import botocore
+ from botocore.exceptions import BotoCoreError, ClientError
+except ImportError:
+ pass # caught by imported HAS_BOTO3
+
+
+class ACMServiceManager(object):
+ """Handles ACM Facts Services"""
+
+ def __init__(self, module):
+ self.module = module
+
+ region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
+ self.client = module.client('acm')
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def delete_certificate_with_backoff(self, client, arn):
+ client.delete_certificate(CertificateArn=arn)
+
+ def delete_certificate(self, client, module, arn):
+ module.debug("Attempting to delete certificate %s" % arn)
+ try:
+ self.delete_certificate_with_backoff(client, arn)
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't delete certificate %s" % arn)
+ module.debug("Successfully deleted certificate %s" % arn)
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def list_certificates_with_backoff(self, client, statuses=None):
+ paginator = client.get_paginator('list_certificates')
+ kwargs = dict()
+ if statuses:
+ kwargs['CertificateStatuses'] = statuses
+ return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList']
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def get_certificate_with_backoff(self, client, certificate_arn):
+ response = client.get_certificate(CertificateArn=certificate_arn)
+ # strip out response metadata
+ return {'Certificate': response['Certificate'],
+ 'CertificateChain': response['CertificateChain']}
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def describe_certificate_with_backoff(self, client, certificate_arn):
+ return client.describe_certificate(CertificateArn=certificate_arn)['Certificate']
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def list_certificate_tags_with_backoff(self, client, certificate_arn):
+ return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags']
+
+ # Returns a list of certificates
+ # if domain_name is specified, returns only certificates with that domain
+ # if an ARN is specified, returns only that certificate
+ # only_tags is a dict, e.g. {'key':'value'}. If specified this function will return
+ # only certificates which contain all those tags (key exists, value matches).
+ def get_certificates(self, client, module, domain_name=None, statuses=None, arn=None, only_tags=None):
+ try:
+ all_certificates = self.list_certificates_with_backoff(client=client, statuses=statuses)
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't obtain certificates")
+ if domain_name:
+ certificates = [cert for cert in all_certificates
+ if cert['DomainName'] == domain_name]
+ else:
+ certificates = all_certificates
+
+ if arn:
+ # still return a list, not just one item
+ certificates = [c for c in certificates if c['CertificateArn'] == arn]
+
+ results = []
+ for certificate in certificates:
+ try:
+ cert_data = self.describe_certificate_with_backoff(client, certificate['CertificateArn'])
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't obtain certificate metadata for domain %s" % certificate['DomainName'])
+
+ # in some states, ACM resources do not have a corresponding cert
+ if cert_data['Status'] not in ['PENDING_VALIDATION', 'VALIDATION_TIMED_OUT', 'FAILED']:
+ try:
+ cert_data.update(self.get_certificate_with_backoff(client, certificate['CertificateArn']))
+ except (BotoCoreError, ClientError, KeyError) as e:
+ if e.response['Error']['Code'] != "RequestInProgressException":
+ module.fail_json_aws(e, msg="Couldn't obtain certificate data for domain %s" % certificate['DomainName'])
+ cert_data = camel_dict_to_snake_dict(cert_data)
+ try:
+ tags = self.list_certificate_tags_with_backoff(client, certificate['CertificateArn'])
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't obtain tags for domain %s" % certificate['DomainName'])
+
+ cert_data['tags'] = boto3_tag_list_to_ansible_dict(tags)
+ results.append(cert_data)
+
+ if only_tags:
+ for tag_key in only_tags:
+ try:
+ results = [c for c in results if ('tags' in c) and (tag_key in c['tags']) and (c['tags'][tag_key] == only_tags[tag_key])]
+ except (TypeError, AttributeError) as e:
+ for c in results:
+ if 'tags' not in c:
+ module.debug("cert is %s" % str(c))
+ module.fail_json(msg="ACM tag filtering err", exception=e)
+
+ return results
+
+ # returns the domain name of a certificate (encoded in the public cert)
+ # for a given ARN
+ # A cert with that ARN must already exist
+ def get_domain_of_cert(self, client, module, arn):
+ if arn is None:
+ module.fail(msg="Internal error with ACM domain fetching, no certificate ARN specified")
+ try:
+ cert_data = self.describe_certificate_with_backoff(client=client, certificate_arn=arn)
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't obtain certificate data for arn %s" % arn)
+ return cert_data['DomainName']
+
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def import_certificate_with_backoff(self, client, certificate, private_key, certificate_chain, arn):
+ if certificate_chain:
+ if arn:
+ ret = client.import_certificate(Certificate=to_bytes(certificate),
+ PrivateKey=to_bytes(private_key),
+ CertificateChain=to_bytes(certificate_chain),
+ CertificateArn=arn)
+ else:
+ ret = client.import_certificate(Certificate=to_bytes(certificate),
+ PrivateKey=to_bytes(private_key),
+ CertificateChain=to_bytes(certificate_chain))
+ else:
+ if arn:
+ ret = client.import_certificate(Certificate=to_bytes(certificate),
+ PrivateKey=to_bytes(private_key),
+ CertificateArn=arn)
+ else:
+ ret = client.import_certificate(Certificate=to_bytes(certificate),
+ PrivateKey=to_bytes(private_key))
+ return ret['CertificateArn']
+
+ # Tags are a normal Ansible style dict
+ # {'Key':'Value'}
+ @AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
+ def tag_certificate_with_backoff(self, client, arn, tags):
+ aws_tags = ansible_dict_to_boto3_tag_list(tags)
+ client.add_tags_to_certificate(CertificateArn=arn, Tags=aws_tags)
+
+ def import_certificate(self, client, module, certificate, private_key, arn=None, certificate_chain=None, tags=None):
+
+ original_arn = arn
+
+ # upload cert
+ try:
+ arn = self.import_certificate_with_backoff(client, certificate, private_key, certificate_chain, arn)
+ except (BotoCoreError, ClientError) as e:
+ module.fail_json_aws(e, msg="Couldn't upload new certificate")
+
+ if original_arn and (arn != original_arn):
+ # I'm not sure whether the API guarentees that the ARN will not change
+ # I'm failing just in case.
+ # If I'm wrong, I'll catch it in the integration tests.
+ module.fail_json(msg="ARN changed with ACM update, from %s to %s" % (original_arn, arn))
+
+ # tag that cert
+ try:
+ self.tag_certificate_with_backoff(client, arn, tags)
+ except (BotoCoreError, ClientError) as e:
+ module.debug("Attempting to delete the cert we just created, arn=%s" % arn)
+ try:
+ self.delete_certificate_with_backoff(client, arn)
+ except Exception as f:
+ module.warn("Certificate %s exists, and is not tagged. So Ansible will not see it on the next run.")
+ module.fail_json_aws(e, msg="Couldn't tag certificate %s, couldn't delete it either" % arn)
+ module.fail_json_aws(e, msg="Couldn't tag certificate %s" % arn)
+
+ return arn
diff --git a/lib/ansible/modules/cloud/amazon/aws_acm.py b/lib/ansible/modules/cloud/amazon/aws_acm.py
new file mode 100644
index 0000000000..baef9f7f78
--- /dev/null
+++ b/lib/ansible/modules/cloud/amazon/aws_acm.py
@@ -0,0 +1,403 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# - Matthew Davis <Matthew.Davis.2@team.telstra.com>
+# on behalf of Telstra Corporation Limited
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+module: aws_acm
+short_description: Upload and delete certificates in the AWS Certificate Manager service
+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.
+ - 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.
+ It does this through applying AWS resource "Name" tags to ACM certificates.
+ - >
+ When C(state=present),
+ if there is one certificate in ACM
+ with a C(Name) tag equal to the C(name_tag) parameter,
+ and an identical body and chain,
+ this task will succeed without effect.
+ - >
+ When C(state=present),
+ if there is one certificate in ACM
+ a C(Name) tag equal to the C(name_tag) parameter,
+ and a different body,
+ this task will overwrite that certificate.
+ - >
+ When C(state=present),
+ if there are multiple certificates in ACM
+ with a C(Name) tag equal to the C(name_tag) parameter,
+ this task will fail.
+ - >
+ When C(state=absent) and C(certificate_arn) is defined,
+ this module will delete the ACM resource with that ARN if it exists in this region,
+ and succeed without effect if it doesn't exist.
+ - >
+ When C(state=absent) and C(domain_name) is defined,
+ this module will delete all ACM resources in this AWS region with a corresponding domain name.
+ If there are none, it will succeed without effect.
+ - >
+ When C(state=absent) and C(certificate_arn) is not defined,
+ and C(domain_name) is not defined,
+ this module will delete all ACM resources in this AWS region with a corresponding C(Name) tag.
+ If there are none, it will succeed without effect.
+ - Note that this may not work properly with keys of size 4096 bits, due to a limitation of the ACM API.
+version_added: "2.10"
+options:
+ certificate:
+ description:
+ - The body of the PEM encoded public certificate.
+ - Required when C(state) is not C(absent).
+ - 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 C(state=present).
+ - If C(state=absent), you must provide one of C(certificate_arn), C(domain_name) or C(name_tag).
+ - >
+ If C(state=absent) and no resource exists with this ARN in this region,
+ the task will succeed with no effect.
+ - >
+ If C(state=absent) and the corresponding resource exists in a different region,
+ this task may report success without deleting that resource.
+ type: str
+
+ certificate_chain:
+ description:
+ - The body of the PEM encoded chain for your certificate.
+ - If your certificate chain is in a file, use C(lookup('file', 'path/to/chain.pem')).
+ - Ignored when C(state=absent)
+ type: str
+
+ domain_name:
+ description:
+ - The domain name of the certificate.
+ - >
+ If C(state=absent) and C(domain_name) is specified,
+ this task will delete all ACM certificates with this domain.
+ - Exactly one of C(domain_name), C(name_tag) and C(certificate_arn) must be provided.
+ - >
+ If C(state=present) this must not be specified.
+ (Since the domain name is encoded within the public certificate's body.)
+ type: str
+
+ name_tag:
+ description:
+ - The unique identifier for tagging resources using AWS tags, with key C(Name).
+ - This can be any set of characters accepted by AWS for tag values.
+ - >
+ This is to ensure Ansible can treat certificates idempotently,
+ even though the ACM API allows duplicate certificates.
+ - If C(state=preset), this must be specified.
+ - >
+ If C(state=absent), you must provide exactly one of
+ C(certificate_arn), C(domain_name) or C(name_tag).
+ type: str
+
+ private_key:
+ description:
+ - The body of the PEM encoded private key.
+ - Required when C(state) is C(present).
+ - Ignored when C(state) is C(absent).
+ - If your private key is in a file, use C(lookup('file', 'path/to/key.pem')).
+ type: str
+
+ state:
+ description:
+ - >
+ If C(state=present), the specified public certificate and private key
+ will be uploaded, with C(Name) tag equal to C(name_tag).
+ - >
+ If C(state=absent), any certificates in this region
+ with a corresponding C(domain_name), C(name_tag) or C(certificate_arn)
+ will be deleted.
+ choices: [present, absent]
+ default: present
+ type: str
+requirements:
+ - boto3
+author:
+ - Matthew Davis (@matt-telstra) on behalf of Telstra Corporation Limited
+extends_documentation_fragment:
+ - aws
+ - ec2
+'''
+
+EXAMPLES = '''
+
+- name: upload a self-signed certificate
+ aws_acm:
+ certificate: "{{ lookup('file', 'cert.pem' ) }}"
+ privateKey: "{{ lookup('file', 'key.pem' ) }}"
+ name_tag: my_cert # to be applied through an AWS tag as "Name":"my_cert"
+ region: ap-southeast-2 # AWS region
+
+- name: create/update a certificate with a chain
+ aws_acm:
+ certificate: "{{ lookup('file', 'cert.pem' ) }}"
+ privateKey: "{{ lookup('file', 'key.pem' ) }}"
+ name_tag: my_cert
+ certificate_chain: "{{ lookup('file', 'chain.pem' ) }}"
+ state: present
+ region: ap-southeast-2
+ register: cert_create
+
+- name: print ARN of cert we just created
+ debug:
+ var: cert_create.certificate.arn
+
+- name: delete the cert we just created
+ aws_acm:
+ name_tag: my_cert
+ state: absent
+ region: ap-southeast-2
+
+- name: delete a certificate with a particular ARN
+ aws_acm:
+ certificate_arn: "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901"
+ state: absent
+ region: ap-southeast-2
+
+- name: delete all certificates with a particular domain name
+ aws_acm:
+ domain_name: acm.ansible.com
+ state: absent
+ region: ap-southeast-2
+
+'''
+
+RETURN = '''
+certificate:
+ description: Information about the certificate which was uploaded
+ type: complex
+ returned: when C(state) is C(present)
+ contains:
+ arn:
+ description: The ARN of the certificate in ACM
+ type: str
+ returned: when C(state) is C(present)
+ sample: "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901"
+ domain_name:
+ description: The domain name encoded within the public certificate
+ type: str
+ returned: when C(state) is C(present)
+ sample: acm.ansible.com
+arns:
+ description: A list of the ARNs of the certificates in ACM which were deleted
+ type: list
+ returned: when C(state) is C(absent)
+ sample:
+ - "arn:aws:acm:ap-southeast-2:123456789012:certificate/01234567-abcd-abcd-abcd-012345678901"
+'''
+
+
+from ansible.module_utils.aws.core import AnsibleAWSModule
+from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info
+from ansible.module_utils.aws.acm import ACMServiceManager
+from ansible.module_utils._text import to_bytes, to_text
+from ssl import PEM_cert_to_DER_cert
+import base64
+import traceback
+import re # regex library
+
+
+# Takes in two text arguments
+# Each a PEM encoded certificate
+# Or a chain of PEM encoded certificates
+# May include some lines between each chain in the cert, e.g. "Subject: ..."
+# Returns True iff the chains/certs are functionally identical (including chain order)
+def chain_compare(module, a, b):
+
+ chain_a_pem = pem_chain_split(module, a)
+ chain_b_pem = pem_chain_split(module, b)
+
+ if len(chain_a_pem) != len(chain_b_pem):
+ return False
+
+ # Chain length is the same
+ for (ca, cb) in zip(chain_a_pem, chain_b_pem):
+ der_a = PEM_body_to_DER(module, ca)
+ der_b = PEM_body_to_DER(module, cb)
+ if der_a != der_b:
+ return False
+
+ return True
+
+
+# Takes in PEM encoded data with no headers
+# returns equivilent DER as byte array
+def PEM_body_to_DER(module, pem):
+ try:
+ der = base64.b64decode(to_text(pem))
+ except (ValueError, TypeError) as e:
+ module.fail_json(msg="Unable to decode certificate chain",
+ exception=traceback.format_exc())
+ return der
+
+
+# Store this globally to avoid repeated recompilation
+pem_chain_split_regex = re.compile(r"------?BEGIN [A-Z0-9. ]*CERTIFICATE------?([a-zA-Z0-9\+\/=\s]+)------?END [A-Z0-9. ]*CERTIFICATE------?")
+
+
+# Use regex to split up a chain or single cert into an array of base64 encoded data
+# Using "-----BEGIN CERTIFICATE-----" and "----END CERTIFICATE----"
+# Noting that some chains have non-pem data in between each cert
+# This function returns only what's between the headers, excluding the headers
+def pem_chain_split(module, pem):
+
+ pem_arr = re.findall(pem_chain_split_regex, to_text(pem))
+
+ if len(pem_arr) == 0:
+ # This happens if the regex doesn't match at all
+ module.fail_json(msg="Unable to split certificate chain. Possibly zero-length chain?")
+
+ return pem_arr
+
+
+def main():
+ argument_spec = dict(
+ certificate=dict(),
+ certificate_arn=dict(alias=['arn']),
+ certificate_chain=dict(),
+ domain_name=dict(alias=['domain']),
+ name_tag=dict(alias=['name']),
+ private_key=dict(no_log=True),
+ state=dict(default='present', choices=['present', 'absent'])
+ )
+ module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
+ acm = ACMServiceManager(module)
+
+ # Check argument requirements
+ if module.params['state'] == 'present':
+ if not module.params['certificate']:
+ module.fail_json(msg="Parameter 'certificate' must be specified if 'state' is specified as 'present'")
+ elif module.params['certificate_arn']:
+ module.fail_json(msg="Parameter 'certificate_arn' is only valid if parameter 'state' is specified as 'absent'")
+ elif not module.params['name_tag']:
+ module.fail_json(msg="Parameter 'name_tag' must be specified if parameter 'state' is specified as 'present'")
+ elif not module.params['private_key']:
+ module.fail_json(msg="Parameter 'private_key' must be specified if 'state' is specified as 'present'")
+ 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
+
+ region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
+ client = boto3_conn(module, conn_type='client', resource='acm',
+ region=region, endpoint=ec2_url, **aws_connect_kwargs)
+
+ # 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)
+
+ 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")
+
+ # 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.")
+ 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)
+
+ else: # state == absent
+ for cert in certificates:
+ acm.delete_certificate(client, module, cert['certificate_arn'])
+ module.exit_json(arns=[cert['certificate_arn'] for cert in certificates],
+ changed=(len(certificates) > 0))
+
+
+if __name__ == '__main__':
+ # tests()
+ main()
diff --git a/lib/ansible/modules/cloud/amazon/aws_acm_info.py b/lib/ansible/modules/cloud/amazon/aws_acm_info.py
index 63ec193f5e..62f301e8d5 100644
--- a/lib/ansible/modules/cloud/amazon/aws_acm_info.py
+++ b/lib/ansible/modules/cloud/amazon/aws_acm_info.py
@@ -15,8 +15,18 @@ short_description: Retrieve certificate information from AWS Certificate Manager
description:
- Retrieve information for ACM certificates
- This module was called C(aws_acm_facts) before Ansible 2.9. The usage did not change.
+ - Note that this will not return information about uploaded keys of size 4096 bits, due to a limitation of the ACM API.
version_added: "2.5"
options:
+ certificate_arn:
+ description:
+ - If provided, the results will be filtered to show only the certificate with this ARN.
+ - If no certificate with this ARN exists, this task will fail.
+ - If a certificate with this ARN exists in a different region, this task will fail
+ aliases:
+ - arn
+ version_added: '2.10'
+ type: str
domain_name:
description:
- The domain name of an ACM certificate to limit the search to
@@ -29,6 +39,11 @@ options:
choices: ['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT', 'REVOKED', 'FAILED']
type: list
elements: str
+ tags:
+ description:
+ - Filter results to show only certificates with tags that match all the tags specified here.
+ type: dict
+ version_added: '2.10'
requirements:
- boto3
author:
@@ -50,6 +65,19 @@ EXAMPLES = '''
aws_acm_info:
statuses:
- PENDING_VALIDATION
+
+- name: obtain all certificates with tag Name=foo and myTag=bar
+ aws_acm_info:
+ tags:
+ Name: foo
+ myTag: bar
+
+
+# The output is still a list of certificates, just one item long.
+- name: obtain information about a certificate with a particular ARN
+ aws_acm_info:
+ certificate_arn: "arn:aws:acm:ap-southeast-2:123456789876:certificate/abcdeabc-abcd-1234-4321-abcdeabcde12"
+
'''
RETURN = '''
@@ -232,106 +260,37 @@ certificates:
type: str
'''
-import traceback
-
-from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info
-from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, HAS_BOTO3, boto3_tag_list_to_ansible_dict
-
-
-try:
- import botocore
-except ImportError:
- pass # caught by imported HAS_BOTO3
-
-
-@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
-def list_certificates_with_backoff(client, statuses=None):
- paginator = client.get_paginator('list_certificates')
- kwargs = dict()
- if statuses:
- kwargs['CertificateStatuses'] = statuses
- return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList']
-
-
-@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
-def get_certificate_with_backoff(client, certificate_arn):
- response = client.get_certificate(CertificateArn=certificate_arn)
- # strip out response metadata
- return {'Certificate': response['Certificate'],
- 'CertificateChain': response['CertificateChain']}
-
-
-@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
-def describe_certificate_with_backoff(client, certificate_arn):
- return client.describe_certificate(CertificateArn=certificate_arn)['Certificate']
-
-
-@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
-def list_certificate_tags_with_backoff(client, certificate_arn):
- return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags']
-
-
-def get_certificates(client, module, domain_name=None, statuses=None):
- try:
- all_certificates = list_certificates_with_backoff(client, statuses)
- except botocore.exceptions.ClientError as e:
- module.fail_json(msg="Couldn't obtain certificates",
- exception=traceback.format_exc(),
- **camel_dict_to_snake_dict(e.response))
- if domain_name:
- certificates = [cert for cert in all_certificates
- if cert['DomainName'] == domain_name]
- else:
- certificates = all_certificates
-
- results = []
- for certificate in certificates:
- try:
- cert_data = describe_certificate_with_backoff(client, certificate['CertificateArn'])
- except botocore.exceptions.ClientError as e:
- module.fail_json(msg="Couldn't obtain certificate metadata for domain %s" % certificate['DomainName'],
- exception=traceback.format_exc(),
- **camel_dict_to_snake_dict(e.response))
- try:
- cert_data.update(get_certificate_with_backoff(client, certificate['CertificateArn']))
- except botocore.exceptions.ClientError as e:
- if e.response['Error']['Code'] != "RequestInProgressException":
- module.fail_json(msg="Couldn't obtain certificate data for domain %s" % certificate['DomainName'],
- exception=traceback.format_exc(),
- **camel_dict_to_snake_dict(e.response))
- cert_data = camel_dict_to_snake_dict(cert_data)
- try:
- tags = list_certificate_tags_with_backoff(client, certificate['CertificateArn'])
- except botocore.exceptions.ClientError as e:
- module.fail_json(msg="Couldn't obtain tags for domain %s" % certificate['DomainName'],
- exception=traceback.format_exc(),
- **camel_dict_to_snake_dict(e.response))
- cert_data['tags'] = boto3_tag_list_to_ansible_dict(tags)
- results.append(cert_data)
- return results
+from ansible.module_utils.aws.acm import ACMServiceManager
def main():
- argument_spec = ec2_argument_spec()
- argument_spec.update(
- dict(
- domain_name=dict(aliases=['name']),
- statuses=dict(type='list', choices=['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT', 'REVOKED', 'FAILED']),
- )
+ argument_spec = dict(
+ certificate_arn=dict(aliases=['arn']),
+ domain_name=dict(aliases=['name']),
+ statuses=dict(type='list', choices=['PENDING_VALIDATION', 'ISSUED', 'INACTIVE', 'EXPIRED', 'VALIDATION_TIMED_OUT', 'REVOKED', 'FAILED']),
+ tags=dict(type='dict'),
)
- module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
+ acm_info = ACMServiceManager(module)
+
if module._name == 'aws_acm_facts':
module.deprecate("The 'aws_acm_facts' module has been renamed to 'aws_acm_info'", version='2.13')
- if not HAS_BOTO3:
- module.fail_json(msg='boto3 and botocore are required by this module')
-
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
client = boto3_conn(module, conn_type='client', resource='acm',
region=region, endpoint=ec2_url, **aws_connect_kwargs)
- certificates = get_certificates(client, module, domain_name=module.params['domain_name'], statuses=module.params['statuses'])
+ certificates = acm_info.get_certificates(client, module,
+ domain_name=module.params['domain_name'],
+ statuses=module.params['statuses'],
+ arn=module.params['certificate_arn'],
+ only_tags=module.params['tags'])
+
+ if module.params['certificate_arn'] and len(certificates) != 1:
+ module.fail_json(msg="No certificate exists in this region with ARN %s" % module.params['certificate_arn'])
+
module.exit_json(certificates=certificates)
diff --git a/test/integration/targets/aws_acm/aliases b/test/integration/targets/aws_acm/aliases
new file mode 100644
index 0000000000..4ea906bd1c
--- /dev/null
+++ b/test/integration/targets/aws_acm/aliases
@@ -0,0 +1,3 @@
+cloud/aws
+aws_acm_info
+unsupported
diff --git a/test/integration/targets/aws_acm/defaults/main.yml b/test/integration/targets/aws_acm/defaults/main.yml
new file mode 100644
index 0000000000..a159302ca5
--- /dev/null
+++ b/test/integration/targets/aws_acm/defaults/main.yml
@@ -0,0 +1,40 @@
+---
+# we'll generate 3 certificates locally for the test
+# Upload the first
+# overwrite it with the second
+# and the third is unrelated, to check we only get info about the first when we want
+local_certs:
+ - priv_key: "{{ remote_tmp_dir }}/private-1.pem"
+ cert: "{{ remote_tmp_dir }}/public-1.pem"
+ csr: "{{ remote_tmp_dir }}/csr-1.csr"
+ domain: acm1.ansible.com
+ name: "{{ resource_prefix }}_1"
+
+ - priv_key: "{{ remote_tmp_dir }}/private-2.pem"
+ cert: "{{ remote_tmp_dir }}/public-2.pem"
+ csr: "{{ remote_tmp_dir }}/csr-2.csr"
+ domain: acm2.ansible.com
+ name: "{{ resource_prefix }}_2"
+
+ - priv_key: "{{ remote_tmp_dir }}/private-3.pem"
+ cert: "{{ remote_tmp_dir }}/public-3.pem"
+ csr: "{{ remote_tmp_dir }}/csr-3.csr"
+ domain: acm3.ansible.com
+ name: "{{ resource_prefix }}_3"
+
+# we'll have one private key
+# make 2 chains using it
+# so we can test what happens when you change just the chain
+# not the domain or key
+chained_cert:
+ priv_key: "{{ remote_tmp_dir }}/private-ch-0.pem"
+ domain: acm-ch.ansible.com
+ name: "{{ resource_prefix }}_4"
+ chains:
+ - cert: "{{ remote_tmp_dir }}/public-ch-0.pem"
+ csr: "{{ remote_tmp_dir }}/csr-ch-0.csr"
+ ca: 0 # index into local_certs
+ - cert: "{{ remote_tmp_dir }}/public-ch-1.pem"
+ csr: "{{ remote_tmp_dir }}/csr-ch-1.csr"
+ ca: 1 # index into local_certs
+ \ No newline at end of file
diff --git a/test/integration/targets/aws_acm/meta/main.yml b/test/integration/targets/aws_acm/meta/main.yml
new file mode 100644
index 0000000000..1810d4bec9
--- /dev/null
+++ b/test/integration/targets/aws_acm/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+ - setup_remote_tmp_dir
diff --git a/test/integration/targets/aws_acm/tasks/full_acm_test.yml b/test/integration/targets/aws_acm/tasks/full_acm_test.yml
new file mode 100644
index 0000000000..64589a945d
--- /dev/null
+++ b/test/integration/targets/aws_acm/tasks/full_acm_test.yml
@@ -0,0 +1,504 @@
+- name: AWS ACM integration test
+ block:
+
+ - set_fact:
+ aws_connection_info: &aws_connection_info
+ aws_region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ security_token: "{{ security_token }}"
+ no_log: True
+
+ # just check this task doesn't fail
+ # I'm not sure if I can assume there aren't already other certs in this account
+ - name: list certs
+ aws_acm_info:
+ <<: *aws_connection_info
+ register: list_all
+ failed_when: list_all.certificates is not defined
+
+ - name: ensure absent cert which doesn't exist - first time
+ aws_acm:
+ <<: *aws_connection_info
+ name_tag: "{{ item.name }}"
+ state: absent
+ with_items: "{{ local_certs }}"
+
+ # just in case it actually existed and was deleted last task
+ # check we don't fail when deleting nothing
+ - name: ensure absent cert which doesn't exist - second time
+ aws_acm:
+ <<: *aws_connection_info
+ name_tag: "{{ item.name }}"
+ state: absent
+ with_items: "{{ local_certs }}"
+ register: absent_start_two
+ failed_when: absent_start_two.changed
+
+ - name: list cert which shouldn't exist
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ item.name }}"
+ register: list_tag
+ with_items: "{{ local_certs }}"
+ failed_when: list_tag.certificates | length > 0
+
+ - name: check directory was made
+ assert:
+ that:
+ - remote_tmp_dir is defined
+
+ # https://github.com/vbotka/ansible-certificate/blob/master/tasks/cert-self-signed.yml
+ - name: Generate private key for local certs
+ openssl_privatekey:
+ path: "{{ item.priv_key }}"
+ type: RSA
+ size: 2048 # ACM doesn't work properly with 4096
+ with_items: "{{ local_certs }}"
+
+ - name: Generate an OpenSSL Certificate Signing Request for own certs
+ openssl_csr:
+ path: "{{ item.csr }}"
+ privatekey_path: "{{ item.priv_key }}"
+ common_name: "{{ item.domain }}"
+ with_items: "{{ local_certs }}"
+
+ - name: Generate a Self Signed OpenSSL certificate for own certs
+ openssl_certificate:
+ provider: selfsigned
+ path: "{{ item.cert }}"
+ csr_path: "{{ item.csr }}"
+ privatekey_path: "{{ item.priv_key }}"
+ signature_algorithms:
+ - 'sha256WithRSAEncryption'
+ # - 'sha512WithRSAEncryption'
+ with_items: "{{ local_certs }}"
+
+ # now upload that certificate
+ - name: upload certificates first time
+ aws_acm:
+ name_tag: "{{ item.name }}"
+ <<: *aws_connection_info
+ certificate: "{{ lookup('file', item.cert ) }}"
+ private_key: "{{ lookup('file', item.priv_key ) }}"
+ state: present
+ register: upload
+ with_items: "{{ local_certs }}"
+ until: upload is succeeded
+ retries: 5
+ delay: 10
+
+ - assert:
+ that:
+ - prev_task.certificate.arn is defined
+ - ('arn:aws:acm:123' | regex_search( 'arn:aws:acm:' )) is defined # check this works like s.startswith('arn')
+ - (prev_task.certificate.arn | regex_search( 'arn:aws:acm:' )) is defined
+ - prev_task.certificate.domain_name == original_cert.domain
+ - prev_task.changed
+ with_items: "{{ upload.results }}"
+ vars:
+ original_cert: "{{ item.item }}"
+ prev_task: "{{ item }}"
+
+ - name: fetch data about cert just uploaded, by ARN
+ aws_acm_info:
+ certificate_arn: "{{ item.certificate.arn }}"
+ <<: *aws_connection_info
+ register: fetch_after_up
+ with_items: "{{ upload.results }}"
+
+ - name: check output of prior task (fetch data about cert just uploaded, by ARN)
+ assert:
+ that:
+ - fetch_after_up_result.certificates | length == 1
+ - fetch_after_up_result.certificates[0].certificate_arn == upload_result.certificate.arn
+ - 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"
+ - fetch_after_up_result.certificates[0].tags['Name'] == original_cert.name
+ with_items: "{{ fetch_after_up.results }}"
+ vars:
+ fetch_after_up_result: "{{ item }}" # corresponding result from task registered as fetch_after_up
+ upload_result: "{{ item.item }}" # corresponding result from task registered as upload
+ original_cert: "{{ item.item.item }}"
+
+ - name: fetch data about cert just uploaded, by name
+ aws_acm_info:
+ tags:
+ Name: "{{ original_cert.name }}"
+ <<: *aws_connection_info
+ register: fetch_after_up_name
+ with_items: "{{ upload.results }}"
+ vars:
+ upload_result: "{{ item }}"
+ original_cert: "{{ item.item }}"
+
+ - name: check fetched data of cert we just uploaded
+ assert:
+ that:
+ - fetch_after_up_name_result.certificates | length == 1
+ - fetch_after_up_name_result.certificates[0].certificate_arn == upload_result.certificate.arn
+ - fetch_after_up_name_result.certificates[0].domain_name == original_cert.domain
+ - (fetch_after_up_name_result.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ (lookup('file', original_cert.cert ) | replace( ' ', '' ) | replace( '\n', ''))
+ - "'Name' in fetch_after_up_name_result.certificates[0].tags"
+ - fetch_after_up_name_result.certificates[0].tags['Name'] == original_cert.name
+ with_items: "{{ fetch_after_up_name.results }}"
+ vars:
+ fetch_after_up_name_result: "{{ item }}" # corresponding result from task registered as fetch_after_up_name
+ upload_result: "{{ item.item }}" # corresponding result from task registered as upload
+ original_cert: "{{ item.item.item }}"
+
+
+ - name: fetch data about cert just uploaded, by domain name
+ aws_acm_info:
+ domain_name: "{{ original_cert.domain }}"
+ <<: *aws_connection_info
+ register: fetch_after_up_domain
+ with_items: "{{ upload.results }}"
+ vars:
+ original_cert: "{{ item.item }}"
+
+ - name: compare fetched data of cert just uploaded to upload task
+ assert:
+ that:
+ - fetch_after_up_domain_result.certificates | length == 1
+ - fetch_after_up_domain_result.certificates[0].certificate_arn == upload_result.certificate.arn
+ - fetch_after_up_domain_result.certificates[0].domain_name == original_cert.domain
+ - (fetch_after_up_domain_result.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ (lookup('file', original_cert.cert ) | replace( ' ', '' ) | replace( '\n', ''))
+ - "'Name' in fetch_after_up_domain_result.certificates[0].tags"
+ - fetch_after_up_domain_result.certificates[0].tags['Name'] == original_cert.name
+ with_items: "{{ fetch_after_up_domain.results }}"
+ vars:
+ fetch_after_up_domain_result: "{{ item }}"
+ upload_result: "{{ item.item }}"
+ original_cert: "{{ item.item.item }}"
+
+
+ # now upload that certificate
+ - name: upload certificates again, check not changed
+ aws_acm:
+ name_tag: "{{ item.name }}"
+ <<: *aws_connection_info
+ certificate: "{{ lookup('file', item.cert ) }}"
+ private_key: "{{ lookup('file', item.priv_key ) }}"
+ state: present
+ register: upload2
+ with_items: "{{ local_certs }}"
+ failed_when: upload2.changed
+
+
+ - name: update first cert with body of the second, first time
+ aws_acm:
+ state: present
+ <<: *aws_connection_info
+ name_tag: "{{ local_certs[0].name }}"
+ certificate: "{{ lookup('file', local_certs[1].cert ) }}"
+ private_key: "{{ lookup('file', local_certs[1].priv_key ) }}"
+ register: overwrite
+
+ - name: check output of previous task (update first cert with body of the second, first time)
+ assert:
+ that:
+ - overwrite.certificate.arn is defined
+ - overwrite.certificate.arn | regex_search( 'arn:aws:acm:' ) is defined
+ - overwrite.certificate.arn == upload.results[0].certificate.arn
+ - overwrite.certificate.domain_name == local_certs[1].domain
+ - overwrite.changed
+
+ - name: check update was sucessfull
+ aws_acm_info:
+ tags:
+ Name: "{{ local_certs[0].name }}"
+ <<: *aws_connection_info
+ register: fetch_after_overwrite
+
+ - name: check output of update fetch
+ assert:
+ that:
+ - fetch_after_overwrite.certificates | length == 1
+ - fetch_after_overwrite.certificates[0].certificate_arn == fetch_after_up.results[0].certificates[0].certificate_arn
+ - fetch_after_overwrite.certificates[0].domain_name == local_certs[1].domain
+ - (fetch_after_overwrite.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', '')) == (lookup('file', local_certs[1].cert )| replace( ' ', '' ) | replace( '\n', ''))
+ - "'Name' in fetch_after_overwrite.certificates[0].tags"
+ - fetch_after_overwrite.certificates[0].tags['Name'] == local_certs[0].name
+
+ - name: fetch other cert
+ aws_acm_info:
+ tags:
+ Name: "{{ local_certs[1].name }}"
+ <<: *aws_connection_info
+ register: check_after_overwrite
+
+ - name: check other cert unaffected
+ assert:
+ that:
+ - check_after_overwrite.certificates | length == 1
+ - check_after_overwrite.certificates[0].certificate_arn == fetch_after_up.results[1].certificates[0].certificate_arn
+ - check_after_overwrite.certificates[0].domain_name == local_certs[1].domain
+ - (check_after_overwrite.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', '')) == (lookup('file', local_certs[1].cert ) | replace( ' ', '' ) | replace( '\n', ''))
+ - "'Name' in check_after_overwrite.certificates[0].tags"
+ - check_after_overwrite.certificates[0].tags['Name'] == local_certs[1].name
+
+ - name: update first cert with body of the second again
+ aws_acm:
+ state: present
+ <<: *aws_connection_info
+ name_tag: "{{ local_certs[0].name }}"
+ certificate: "{{ lookup('file', local_certs[1].cert ) }}"
+ private_key: "{{ lookup('file', local_certs[1].priv_key ) }}"
+ register: overwrite2
+
+ - name: check output of previous task (update first cert with body of the second again)
+ assert:
+ that:
+ - overwrite2.certificate.arn is defined
+ - overwrite2.certificate.arn | regex_search( 'arn:aws:acm:' ) is defined
+ - overwrite2.certificate.arn == upload.results[0].certificate.arn
+ - overwrite2.certificate.domain_name == local_certs[1].domain
+ - not overwrite2.changed
+
+ - name: delete certs 1 and 2
+ aws_acm:
+ <<: *aws_connection_info
+ state: absent
+ domain_name: "{{ local_certs[1].domain }}"
+ register: delete_both
+
+ - name: test prev task
+ assert:
+ that:
+ - delete_both.arns is defined
+ - check_after_overwrite.certificates[0].certificate_arn in delete_both.arns
+ - upload.results[0].certificate.arn in delete_both.arns
+ - delete_both.changed
+
+ - name: fetch info for certs 1 and 2
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ local_certs[item].name }}"
+ register: check_del_one
+ with_items:
+ - 0
+ - 1
+
+ - name: check certs 1 and 2 were already deleted
+ with_items: "{{ check_del_one.results }}"
+ assert:
+ that: item.certificates | length == 0
+
+ - name: check cert 3 not deleted
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ local_certs[2].name }}"
+ register: check_del_one_remain
+ failed_when: check_del_one_remain.certificates | length != 1
+
+ - name: delete cert 3
+ aws_acm:
+ <<: *aws_connection_info
+ state: absent
+ domain_name: "{{ local_certs[2].domain }}"
+ register: delete_third
+
+ - name: check cert 3 deletion went as expected
+ assert:
+ that:
+ - delete_third.arns is defined
+ - delete_third.arns | length == 1
+ - delete_third.arns[0] == upload.results[2].certificate.arn
+ - delete_third.changed
+
+ - name: check cert 3 was deleted
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ local_certs[2].name }}"
+ register: check_del_three
+ failed_when: check_del_three.certificates | length != 0
+
+ - name: delete cert 3 again
+ aws_acm:
+ <<: *aws_connection_info
+ state: absent
+ domain_name: "{{ local_certs[2].domain }}"
+ register: delete_third
+
+ - name: check deletion of cert 3 not changed, because already deleted
+ assert:
+ that:
+ - delete_third.arns is defined
+ - delete_third.arns | length == 0
+ - not delete_third.changed
+
+ - name: check directory was made
+ assert:
+ that:
+ - remote_tmp_dir is defined
+
+ - name: Generate private key for cert to be chained
+ openssl_privatekey:
+ path: "{{ chained_cert.priv_key }}"
+ type: RSA
+ size: 2048 # ACM doesn't work properly with 4096
+
+ - name: Generate two OpenSSL Certificate Signing Requests for cert to be chained
+ openssl_csr:
+ path: "{{ item.csr }}"
+ privatekey_path: "{{ chained_cert.priv_key }}"
+ common_name: "{{ chained_cert.domain }}"
+ with_items: "{{ chained_cert.chains }}"
+
+
+ - name: Sign new certs with cert 0 and 1
+ openssl_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'
+ # - 'sha512WithRSAEncryption'
+ with_items: "{{ chained_cert.chains }}"
+
+ - name: check files exist (for next task)
+ file:
+ path: "{{ item }}"
+ state: file
+ with_items:
+ - "{{ local_certs[chained_cert.chains[0].ca].cert }}"
+ - "{{ local_certs[chained_cert.chains[1].ca].cert }}"
+ - "{{ chained_cert.chains[0].cert }}"
+ - "{{ chained_cert.chains[1].cert }}"
+
+ - name: Find chains
+ certificate_complete_chain:
+ input_chain: "{{ lookup('file', item.cert ) }}"
+ root_certificates:
+ - "{{ local_certs[item.ca].cert }}"
+ with_items: "{{ chained_cert.chains }}"
+ register: chains
+
+ - name: upload chained cert, first chain, first time
+ aws_acm:
+ name_tag: "{{ chained_cert.name }}"
+ <<: *aws_connection_info
+ certificate: "{{ lookup('file', chained_cert.chains[0].cert ) }}"
+ certificate_chain: "{{ chains.results[0].complete_chain | join('\n') }}"
+ private_key: "{{ lookup('file', chained_cert.priv_key ) }}"
+ state: present
+ register: upload_chain
+ failed_when: not upload_chain.changed
+
+ - name: fetch chain of cert we just uploaded
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ chained_cert.name }}"
+ register: check_chain
+
+ - name: check chain of cert we just uploaded
+ assert:
+ that:
+ - (check_chain.certificates[0].certificate_chain | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ ( chains.results[0].complete_chain | join( '\n' ) | replace( ' ', '' ) | replace( '\n', '') )
+ - (check_chain.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ ( lookup('file', chained_cert.chains[0].cert ) | replace( ' ', '' ) | replace( '\n', '') )
+
+ - name: upload chained cert again, check not changed
+ aws_acm:
+ name_tag: "{{ chained_cert.name }}"
+ <<: *aws_connection_info
+ certificate: "{{ lookup('file', chained_cert.chains[0].cert ) }}"
+ certificate_chain: "{{ chains.results[0].complete_chain | join('\n') }}"
+ private_key: "{{ lookup('file', chained_cert.priv_key ) }}"
+ state: present
+ register: upload_chain_2
+
+ - name: check previous task not changed
+ assert:
+ that:
+ - upload_chain_2.certificate.arn == upload_chain.certificate.arn
+ - not upload_chain_2.changed
+
+ - name: upload chained cert, different chain
+ aws_acm:
+ name_tag: "{{ chained_cert.name }}"
+ <<: *aws_connection_info
+ certificate: "{{ lookup('file', chained_cert.chains[1].cert ) }}"
+ certificate_chain: "{{ chains.results[1].complete_chain | join('\n') }}"
+ private_key: "{{ lookup('file', chained_cert.priv_key ) }}"
+ state: present
+ register: upload_chain_3
+
+ - name: check uploading with different chain is changed
+ assert:
+ that:
+ - upload_chain_3.changed
+ - upload_chain_3.certificate.arn == upload_chain.certificate.arn
+
+ - name: fetch info about chain of cert we just updated
+ aws_acm_info:
+ <<: *aws_connection_info
+ tags:
+ Name: "{{ chained_cert.name }}"
+ register: check_chain_2
+
+ - name: check chain of cert we just uploaded
+ assert:
+ that:
+ - (check_chain_2.certificates[0].certificate_chain | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ ( chains.results[1].complete_chain | join( '\n' ) | replace( ' ', '' ) | replace( '\n', '') )
+ - (check_chain_2.certificates[0].certificate | replace( ' ', '' ) | replace( '\n', ''))
+ ==
+ ( lookup('file', chained_cert.chains[1].cert ) | replace( ' ', '' ) | replace( '\n', '') )
+
+ - name: delete chained cert
+ aws_acm:
+ name_tag: "{{ chained_cert.name }}"
+ <<: *aws_connection_info
+ state: absent
+ register: delete_chain_3
+
+ - name: check deletion of chained cert 3 is changed
+ assert:
+ that:
+ - delete_chain_3.changed
+ - upload_chain.certificate.arn in delete_chain_3.arns
+
+
+ always:
+
+ - name: delete first bunch of certificates
+ aws_acm:
+ name_tag: "{{ item.name }}"
+ <<: *aws_connection_info
+ state: absent
+ with_items: "{{ local_certs }}"
+ ignore_errors: yes
+
+ - name: delete chained cert
+ aws_acm:
+ state: absent
+ name_tag: "{{ chained_cert.name }}"
+ <<: *aws_connection_info
+ ignore_errors: yes
+
+
+ - name: deleting local directory with test artefacts
+ file:
+ path: "{{ remote_tmp_dir }}"
+ state: directory
+ ignore_errors: yes \ No newline at end of file
diff --git a/test/integration/targets/aws_acm/tasks/main.yml b/test/integration/targets/aws_acm/tasks/main.yml
new file mode 100644
index 0000000000..4774276fab
--- /dev/null
+++ b/test/integration/targets/aws_acm/tasks/main.yml
@@ -0,0 +1,33 @@
+- name: AWS ACM integration test virtualenv wrapper
+ block:
+ - set_fact:
+ virtualenv: "{{ remote_tmp_dir }}/virtualenv"
+ virtualenv_command: "{{ ansible_python_interpreter }} -m virtualenv"
+
+ - set_fact:
+ virtualenv_interpreter: "{{ virtualenv }}/bin/python"
+
+ - pip:
+ name: virtualenv
+
+ - pip:
+ name:
+ - 'botocore<1.13.0,>=1.12.211'
+ - boto3
+ - coverage
+ - jinja2
+ - pyyaml
+ - 'pyopenssl>=0.15'
+ - 'cryptography>=1.6'
+ virtualenv: "{{ virtualenv }}"
+ virtualenv_command: "{{ virtualenv_command }}"
+ virtualenv_site_packages: no
+
+ - include_tasks: full_acm_test.yml
+ vars:
+ ansible_python_interpreter: "{{ virtualenv_interpreter }}"
+
+ always:
+ - file:
+ path: "{{ virtualenv }}"
+ state: absent \ No newline at end of file
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/a.pem b/test/units/modules/cloud/amazon/fixtures/certs/a.pem
new file mode 100644
index 0000000000..4412f3258a
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/a.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFVTCCBD2gAwIBAgISAx4pnfwvGxYrrQhr/UXiN7HCMA0GCSqGSIb3DQEBCwUA
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA3MjUwMDI4NTdaFw0x
+OTEwMjMwMDI4NTdaMBoxGDAWBgNVBAMTD2NyeXB0b2dyYXBoeS5pbzCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKJDpCL99DVo83587MrVp6gunmKRoUfY
+vcgk5u2v0tB9OmZkcIY37z6AunHWr18Yj55zHmm6G8Nf35hmu3ql2A26WThCbmOe
+WXbxhgarkningZI9opUWnI2dIllguVIsq99GzhpNnDdCb26s5+SRhJI4cr4hYaKC
+XGDKooKWyXUX09SJTq7nW/1+pq3y9ZMvldRKjJALeAdwnC7kmUB6pK7q8J2VlpfQ
+wqGu6q/WHVdgnhWARw3GEFJWDn9wkxBAF08CpzhVaEj+iK+Ut/1HBgNYwqI47h7S
+q+qv0G2qklRVUtEM0zYRsp+y/6vivdbFLlPw8VaerbpJN3gLtpVNcGECAwEAAaOC
+AmMwggJfMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUjbe0bE1aZ8HiqtwqUfCe15bF
+V8UwHwYDVR0jBBgwFoAUqEpqYwR93brm0Tm3pkVl7/Oo7KEwbwYIKwYBBQUHAQEE
+YzBhMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcC5pbnQteDMubGV0c2VuY3J5cHQu
+b3JnMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDMubGV0c2VuY3J5cHQu
+b3JnLzAaBgNVHREEEzARgg9jcnlwdG9ncmFwaHkuaW8wTAYDVR0gBEUwQzAIBgZn
+gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s
+ZXRzZW5jcnlwdC5vcmcwggEDBgorBgEEAdZ5AgQCBIH0BIHxAO8AdgB0ftqDMa0z
+EJEhnM4lT0Jwwr/9XkIgCMY3NXnmEHvMVgAAAWwmvtnXAAAEAwBHMEUCIFXHYX/E
+xtbYCvjjQ3dN0HOLW1d8+aduktmax4mu3KszAiEAvTpxuSVVXJnVGA4tU2GOnI60
+sqTh/IK6hvrFN1k1HBUAdQApPFGWVMg5ZbqqUPxYB9S3b79Yeily3KTDDPTlRUf0
+eAAAAWwmvtm9AAAEAwBGMEQCIDn7sgzD+7JzR+XTvjKf7VyLWwX37O8uwCfCTKo7
++tEhAiB05bHiICU5wkfRBrwcvqXf4bPF7NT5LVlRQYzJ/hbpvzANBgkqhkiG9w0B
+AQsFAAOCAQEAcMU8E6D+5WC07QSeTppRTboC++7YgQg5NiSWm7OE2FlyiRZXnu0Y
+uBoaqAkZIqj7dom9wy1c1UauxOfM9lUZKhYnDTBu9tIhBAvCS0J0avv1j1KQygQ1
+qV+urJsunUwqV/vPWo1GfWophvyXVN6MAycv34ZXZvAjtG7oDcoQVLLvK1SIo2vu
+4/dNkOQzaeZez8q6Ij9762TbBWaK5C789VMdUWZCADWoToPIK533cWbDEp4IhBU/
+K73d7lGGl7S59SjT2V/XE6eJS9Zlj0M+A8pf/8tjM/ImHAjlOHB02sM/VfZ7HAuZ
+61TPxohL+e+X1FYeqIXYGXJmCEuB8WEmBg==
+-----END CERTIFICATE-----
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/b.pem b/test/units/modules/cloud/amazon/fixtures/certs/b.pem
new file mode 100644
index 0000000000..2be4bca533
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/b.pem
@@ -0,0 +1,47 @@
+-----BEGIN CERTIFICATE-----
+MIIIUjCCB/egAwIBAgIRALiJR3zQjp0MevT/Hk89sfAwCgYIKoZIzj0EAwIwgZIx
+CzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNV
+BAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTgwNgYDVQQD
+Ey9DT01PRE8gRUNDIERvbWFpbiBWYWxpZGF0aW9uIFNlY3VyZSBTZXJ2ZXIgQ0Eg
+MjAeFw0xOTA3MzEwMDAwMDBaFw0yMDAyMDYyMzU5NTlaMGwxITAfBgNVBAsTGERv
+bWFpbiBDb250cm9sIFZhbGlkYXRlZDEhMB8GA1UECxMYUG9zaXRpdmVTU0wgTXVs
+dGktRG9tYWluMSQwIgYDVQQDExtzc2wzODczMzcuY2xvdWRmbGFyZXNzbC5jb20w
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARPFdjdnBIJRPnHCPsCBJ/MmPytXnZX
+KV6lD2bbG5EVNuUQln4Na8heCY+sfpV+SPuuiNzZxgDA46GvyzdRYFhxo4IGUTCC
+Bk0wHwYDVR0jBBgwFoAUQAlhZ/C8g3FP3hIILG/U1Ct2PZYwHQYDVR0OBBYEFGLh
+bHk1KAYIRfVwXA3L+yDf0CxjMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA
+MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBPBgNVHSAESDBGMDoGCysG
+AQQBsjEBAgIHMCswKQYIKwYBBQUHAgEWHWh0dHBzOi8vc2VjdXJlLmNvbW9kby5j
+b20vQ1BTMAgGBmeBDAECATBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLmNv
+bW9kb2NhNC5jb20vQ09NT0RPRUNDRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZl
+ckNBMi5jcmwwgYgGCCsGAQUFBwEBBHwwejBRBggrBgEFBQcwAoZFaHR0cDovL2Ny
+dC5jb21vZG9jYTQuY29tL0NPTU9ET0VDQ0RvbWFpblZhbGlkYXRpb25TZWN1cmVT
+ZXJ2ZXJDQTIuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC5jb21vZG9jYTQu
+Y29tMIIDkAYDVR0RBIIDhzCCA4OCG3NzbDM4NzMzNy5jbG91ZGZsYXJlc3NsLmNv
+bYIMKi5hanJ0Y3QuY29tghMqLmFrcmVwYnVyY3UuZ2VuLnRyghUqLmFuZHJlYXNr
+YW5lbGxvcy5jb22CDSouYW5zaWJsZS5jb22CGSouYXJ0b2Z0b3VjaC1raW5nd29v
+ZC5jb22CFyouYm91bGRlcnN3YXRlcmhvbGUuY29tghcqLmJyb2Nrc3RlY2hzdXBw
+b3J0LmNvbYIQKi5idXJjbGFyLndlYi50coIcKi5ob3Blc29uZ2ZyZW5jaGJ1bGxk
+b2dzLm5ldIIMKi5odXJyZW0uY29tghAqLmh5dmVsaWNvbnMuY29tghAqLmthcm1h
+Zml0LmNvLnVrghUqLmxvd3J5c3lzdGVtc2luYy5jb22CDioubWFuaWNydW4uY29t
+ghUqLm11dHVvZmluYW5jaWVyYS5jb22CDyoucGlsZ3JpbWFnZS5waIINKi5wa2dh
+bWVzLm9yZ4IbKi5ybHBjb25zdWx0aW5nc2VydmljZXMuY29tghYqLnJ1eWF0YWJp
+cmxlcmkuZ2VuLnRyghQqLnJ5YW5hcHBoeXNpY3NjLmNvbYIVKi53ZWFyaXRiYWNr
+d2FyZHMub3Jngg8qLnlldGlzbmFjay5jb22CCmFqcnRjdC5jb22CEWFrcmVwYnVy
+Y3UuZ2VuLnRyghNhbmRyZWFza2FuZWxsb3MuY29tggthbnNpYmxlLmNvbYIXYXJ0
+b2Z0b3VjaC1raW5nd29vZC5jb22CFWJvdWxkZXJzd2F0ZXJob2xlLmNvbYIVYnJv
+Y2tzdGVjaHN1cHBvcnQuY29tgg5idXJjbGFyLndlYi50coIaaG9wZXNvbmdmcmVu
+Y2hidWxsZG9ncy5uZXSCCmh1cnJlbS5jb22CDmh5dmVsaWNvbnMuY29tgg5rYXJt
+YWZpdC5jby51a4ITbG93cnlzeXN0ZW1zaW5jLmNvbYIMbWFuaWNydW4uY29tghNt
+dXR1b2ZpbmFuY2llcmEuY29tgg1waWxncmltYWdlLnBoggtwa2dhbWVzLm9yZ4IZ
+cmxwY29uc3VsdGluZ3NlcnZpY2VzLmNvbYIUcnV5YXRhYmlybGVyaS5nZW4udHKC
+EnJ5YW5hcHBoeXNpY3NjLmNvbYITd2Vhcml0YmFja3dhcmRzLm9yZ4INeWV0aXNu
+YWNrLmNvbTCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2ALIeBcyLos2KIE6HZvkr
+uYolIGdr2vpw57JJUy3vi5BeAAABbEVw8SgAAAQDAEcwRQIgE2YeTfb/d4BBUwpZ
+ihWXSR+vRyNNUg8GlOak2MFMHv0CIQCLBvtU401m5/Psg9KirQZs321BSxgUKgSQ
+m9M691d3eQB2AF6nc/nfVsDntTZIfdBJ4DJ6kZoMhKESEoQYdZaBcUVYAAABbEVw
+8VgAAAQDAEcwRQIgGYsGfr3/mekjzMS9+ALAjx1ryfIfhXB/+UghTcw4Y8ICIQDS
+K2L18WX3+Oh4TjJhjh5tV1iYyZVYivcwwbr7mtmOqjAKBggqhkjOPQQDAgNJADBG
+AiEAjNt7LF78GV7snky9jwFcBsLH55ndzduvsrkJ7Ne1SgYCIQDsMJsTr9VP6kar
+4Kv8V9zNBmpGrGNuE7A1GixBvzNaHA==
+-----END CERTIFICATE-----
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-1.0.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.0.cert
new file mode 100644
index 0000000000..6997766acd
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.0.cert
@@ -0,0 +1,121 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+MIIIHTCCBgWgAwIBAgIUCqrrzSfjzaoyB3DOxst2kMxFp/MwDQYJKoZIhvcNAQEL
+BQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyMTIyMjIy
+OFoXDTIxMDgyMTIyMzIwMFowgZsxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBv
+cmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MSYwJAYDVQQD
+DB1kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMPAPH2y206qios2NMzlCNJv1mrwC1/8tH2HOqJGiYZB
+O7QOBRSvJsV++IozCB8ap99e8B64OOAQPOyykrdXd2axhftmMb1SFMF56eukHSuz
+KhKWRUgHs0UFRU51lDcBcOvphwJ+5SOgqrqKFFFBgJ0ZpcP54JpFwKIdh3ac10x2
+mBaW5ccqdv5X9oEMu1D/yivBmy34tsbLYyfttCjP76iVT7UVYHjHWynnIhsEyMsU
+gdM90NzrTlrvTSi/EcCD1W3+8b0f+G1TI5rhHbKwR0n/mv5QLFm7EABoYPhxS8bX
+B+9tE67yb0RyWbgvUiHySRynQLNMRpRx8Y9bA8uC8n8CAwEAAaOCA6QwggOgMAkG
+A1UdEwQCMAAwHwYDVR0jBBgwFoAUsxKJtalLNbwVAPCA6dh4h/ETfHYwcwYIKwYB
+BQUHAQEEZzBlMDcGCCsGAQUFBzAChitodHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9i
+YWwuY29tL3F2c3NsZzMuY3J0MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5xdW92
+YWRpc2dsb2JhbC5jb20wgZ8GA1UdEQSBlzCBlIIdZGV2LmVuZXJneS5pbnNpZGUu
+dGVsc3RyYS5jb22CJXJlcG9ydHMuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5j
+b22CJ2dyZWVuc3luYy5kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbYIjbmdv
+c3MuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wUQYDVR0gBEowSDBGBgwr
+BgEEAb5YAAJkAQEwNjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5xdW92YWRpc2ds
+b2JhbC5jb20vcmVwb3NpdG9yeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwEwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybC5xdW92YWRpc2dsb2JhbC5j
+b20vcXZzc2xnMy5jcmwwHQYDVR0OBBYEFEoJQRpPC/V5ZK3mMkszZE2v6vh+MA4G
+A1UdDwEB/wQEAwIFoDCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFstk9Y+gAABAMARjBEAiBFMZa6
+O9iXVjy2kqQa54vgNFdU7shgFJJhm//fSAQZUAIgBIL/yPdh+XiuQS2xPhCzNYkh
+bxf7BbN4qUISESgiZpsAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZ
+EwAAAWy2T1nKAAAEAwBHMEUCIG0tp63jLsDsfCTDlcvV5ItjRkbUJBnkxlPdP2PH
+88sTAiEApgaPofVdn2hdI12iDDex72ta+9wpwQ1MxoaJn2nt+qEAdQDuS723dc5g
+uuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAWy2T1iJAAAEAwBGMEQCIE/mzEFp
+CJUc71jvwJa4Px86R3ZYK4mHmUlQAUZqd0ZkAiBdEmT8xxTuleSUlYHEkKCK/FZX
+L+vsYJpPrA9TsO5IsTANBgkqhkiG9w0BAQsFAAOCAgEApE9WLz3S8tqA9Dk3r9LF
+rJy8km9cBt1O9SQZwFsduGKGdF3Fd+/Y0V7UrFDzrX+NIzqcmgBHKxaIXorMBF70
+ajMaaROP2ymkpEXnruEwoR47fbW+JRAWDRm2xnouQveQX9ZcgCLbBvAWBqpndQj2
+DGmLJhNz5GlFBjh3PQZlU1w8hU7TrDxa7M1GMtVnk8X+o3l/MX9iPeEs+PiC4dHD
+hpj84RY1VQJz8+10rql47SB5YgbwcqaizTG4ax/OAv1JHNWtfAodIMX8Y8X00zoz
+A20LQv880jCCNANVNbrXJ3h4X3xwW/C1X9vYk0shymZJbT5u17JbPD1cy39bA7kT
+F4L7scdQRxvcqazYN4/IdgvgMji9OltiYufP88Ti8KB2tcl2accpiC5St/zllGD1
+hqEeYLMzjyvUKR/1uvURQQtc0DPvBRmvkB+aI4g+sLkTTFWj5bsA1vKU8SDCyMuB
+RQV11DId5+RNNCmWnskORUZJQssvY49pnfCxCES2nt3l/XzTzVtLYmd6G9uAqVac
+e2ibnmDrFVlmlyRsCiMfZl5/OTJzt7Cj3az59m5Syfw/lnS9YP82t/r/ufuKkO5Q
+q5a9aI8DuNNmAjR4lpIJNqIpX/y+dG2aGmx4XTc31MR9szWtiTgOHe0MkMupOAL0
+qkHrBgwo1zjuTMf3QOg6Z5Q=
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0y
+MjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vK
+EymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPan
+H05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jz
+kE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5B
+xxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uD
+JGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNV
+HSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1Ud
+IwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4E
+FgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/A
+Vn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTs
+Y4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48
+gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/
+TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMb
+SXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-1.1.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.1.cert
new file mode 100644
index 0000000000..51f64f08d1
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.1.cert
@@ -0,0 +1,69 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+MIIIHTCCBgWgAwIBAgIUCqrrzSfjzaoyB3DOxst2kMxFp/MwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyMTIyMjIyOFoXDTIxMDgyMTIyMzIwMFowgZsxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBvcmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MSYwJAYDVQQD
+DB1kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPAPH2y206qios2NMzlCNJv1mrwC1/8tH2HOqJGiYZB
+O7QOBRSvJsV++IozCB8ap99e8B64OOAQPOyykrdXd2axhftmMb1SFMF56eukHSuzKhKWRUgHs0UFRU51lDcBcOvphwJ+5SOgqrqKFFFBgJ0ZpcP54JpFwKIdh3ac10x2
+mBaW5ccqdv5X9oEMu1D/yivBmy34tsbLYyfttCjP76iVT7UVYHjHWynnIhsEyMsUgdM90NzrTlrvTSi/EcCD1W3+8b0f+G1TI5rhHbKwR0n/mv5QLFm7EABoYPhxS8bX
+B+9tE67yb0RyWbgvUiHySRynQLNMRpRx8Y9bA8uC8n8CAwEAAaOCA6QwggOgMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUsxKJtalLNbwVAPCA6dh4h/ETfHYwcwYIKwYB
+BQUHAQEEZzBlMDcGCCsGAQUFBzAChitodHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9iYWwuY29tL3F2c3NsZzMuY3J0MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5xdW92
+YWRpc2dsb2JhbC5jb20wgZ8GA1UdEQSBlzCBlIIdZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb22CJXJlcG9ydHMuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5j
+b22CJ2dyZWVuc3luYy5kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbYIjbmdvc3MuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wUQYDVR0gBEowSDBGBgwr
+BgEEAb5YAAJkAQEwNjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5xdW92YWRpc2dsb2JhbC5jb20vcmVwb3NpdG9yeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwEwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybC5xdW92YWRpc2dsb2JhbC5jb20vcXZzc2xnMy5jcmwwHQYDVR0OBBYEFEoJQRpPC/V5ZK3mMkszZE2v6vh+MA4G
+A1UdDwEB/wQEAwIFoDCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAVhQGmi/XwuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFstk9Y+gAABAMARjBEAiBFMZa6
+O9iXVjy2kqQa54vgNFdU7shgFJJhm//fSAQZUAIgBIL/yPdh+XiuQS2xPhCzNYkhbxf7BbN4qUISESgiZpsAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZ
+EwAAAWy2T1nKAAAEAwBHMEUCIG0tp63jLsDsfCTDlcvV5ItjRkbUJBnkxlPdP2PH88sTAiEApgaPofVdn2hdI12iDDex72ta+9wpwQ1MxoaJn2nt+qEAdQDuS723dc5g
+uuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAWy2T1iJAAAEAwBGMEQCIE/mzEFpCJUc71jvwJa4Px86R3ZYK4mHmUlQAUZqd0ZkAiBdEmT8xxTuleSUlYHEkKCK/FZX
+L+vsYJpPrA9TsO5IsTANBgkqhkiG9w0BAQsFAAOCAgEApE9WLz3S8tqA9Dk3r9LFrJy8km9cBt1O9SQZwFsduGKGdF3Fd+/Y0V7UrFDzrX+NIzqcmgBHKxaIXorMBF70
+ajMaaROP2ymkpEXnruEwoR47fbW+JRAWDRm2xnouQveQX9ZcgCLbBvAWBqpndQj2DGmLJhNz5GlFBjh3PQZlU1w8hU7TrDxa7M1GMtVnk8X+o3l/MX9iPeEs+PiC4dHD
+hpj84RY1VQJz8+10rql47SB5YgbwcqaizTG4ax/OAv1JHNWtfAodIMX8Y8X00zozA20LQv880jCCNANVNbrXJ3h4X3xwW/C1X9vYk0shymZJbT5u17JbPD1cy39bA7kT
+F4L7scdQRxvcqazYN4/IdgvgMji9OltiYufP88Ti8KB2tcl2accpiC5St/zllGD1hqEeYLMzjyvUKR/1uvURQQtc0DPvBRmvkB+aI4g+sLkTTFWj5bsA1vKU8SDCyMuB
+RQV11DId5+RNNCmWnskORUZJQssvY49pnfCxCES2nt3l/XzTzVtLYmd6G9uAqVace2ibnmDrFVlmlyRsCiMfZl5/OTJzt7Cj3az59m5Syfw/lnS9YP82t/r/ufuKkO5Q
+q5a9aI8DuNNmAjR4lpIJNqIpX/y+dG2aGmx4XTc31MR9szWtiTgOHe0MkMupOAL0qkHrBgwo1zjuTMf3QOg6Z5Q=
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQELBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0yMjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vKEymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPanH05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jzkE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5BxxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uDJGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNVHSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1UdIwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4EFgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/AVn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTsY4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMbSXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-1.2.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.2.cert
new file mode 100644
index 0000000000..ce29924118
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.2.cert
@@ -0,0 +1,113 @@
+-----BEGIN CERTIFICATE-----
+MIIIHTCCBgWgAwIBAgIUCqrrzSfjzaoyB3DOxst2kMxFp/MwDQYJKoZIhvcNAQEL
+BQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyMTIyMjIy
+OFoXDTIxMDgyMTIyMzIwMFowgZsxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBv
+cmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MSYwJAYDVQQD
+DB1kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMPAPH2y206qios2NMzlCNJv1mrwC1/8tH2HOqJGiYZB
+O7QOBRSvJsV++IozCB8ap99e8B64OOAQPOyykrdXd2axhftmMb1SFMF56eukHSuz
+KhKWRUgHs0UFRU51lDcBcOvphwJ+5SOgqrqKFFFBgJ0ZpcP54JpFwKIdh3ac10x2
+mBaW5ccqdv5X9oEMu1D/yivBmy34tsbLYyfttCjP76iVT7UVYHjHWynnIhsEyMsU
+gdM90NzrTlrvTSi/EcCD1W3+8b0f+G1TI5rhHbKwR0n/mv5QLFm7EABoYPhxS8bX
+B+9tE67yb0RyWbgvUiHySRynQLNMRpRx8Y9bA8uC8n8CAwEAAaOCA6QwggOgMAkG
+A1UdEwQCMAAwHwYDVR0jBBgwFoAUsxKJtalLNbwVAPCA6dh4h/ETfHYwcwYIKwYB
+BQUHAQEEZzBlMDcGCCsGAQUFBzAChitodHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9i
+YWwuY29tL3F2c3NsZzMuY3J0MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5xdW92
+YWRpc2dsb2JhbC5jb20wgZ8GA1UdEQSBlzCBlIIdZGV2LmVuZXJneS5pbnNpZGUu
+dGVsc3RyYS5jb22CJXJlcG9ydHMuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5j
+b22CJ2dyZWVuc3luYy5kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbYIjbmdv
+c3MuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wUQYDVR0gBEowSDBGBgwr
+BgEEAb5YAAJkAQEwNjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5xdW92YWRpc2ds
+b2JhbC5jb20vcmVwb3NpdG9yeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwEwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybC5xdW92YWRpc2dsb2JhbC5j
+b20vcXZzc2xnMy5jcmwwHQYDVR0OBBYEFEoJQRpPC/V5ZK3mMkszZE2v6vh+MA4G
+A1UdDwEB/wQEAwIFoDCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFstk9Y+gAABAMARjBEAiBFMZa6
+O9iXVjy2kqQa54vgNFdU7shgFJJhm//fSAQZUAIgBIL/yPdh+XiuQS2xPhCzNYkh
+bxf7BbN4qUISESgiZpsAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZ
+EwAAAWy2T1nKAAAEAwBHMEUCIG0tp63jLsDsfCTDlcvV5ItjRkbUJBnkxlPdP2PH
+88sTAiEApgaPofVdn2hdI12iDDex72ta+9wpwQ1MxoaJn2nt+qEAdQDuS723dc5g
+uuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAWy2T1iJAAAEAwBGMEQCIE/mzEFp
+CJUc71jvwJa4Px86R3ZYK4mHmUlQAUZqd0ZkAiBdEmT8xxTuleSUlYHEkKCK/FZX
+L+vsYJpPrA9TsO5IsTANBgkqhkiG9w0BAQsFAAOCAgEApE9WLz3S8tqA9Dk3r9LF
+rJy8km9cBt1O9SQZwFsduGKGdF3Fd+/Y0V7UrFDzrX+NIzqcmgBHKxaIXorMBF70
+ajMaaROP2ymkpEXnruEwoR47fbW+JRAWDRm2xnouQveQX9ZcgCLbBvAWBqpndQj2
+DGmLJhNz5GlFBjh3PQZlU1w8hU7TrDxa7M1GMtVnk8X+o3l/MX9iPeEs+PiC4dHD
+hpj84RY1VQJz8+10rql47SB5YgbwcqaizTG4ax/OAv1JHNWtfAodIMX8Y8X00zoz
+A20LQv880jCCNANVNbrXJ3h4X3xwW/C1X9vYk0shymZJbT5u17JbPD1cy39bA7kT
+F4L7scdQRxvcqazYN4/IdgvgMji9OltiYufP88Ti8KB2tcl2accpiC5St/zllGD1
+hqEeYLMzjyvUKR/1uvURQQtc0DPvBRmvkB+aI4g+sLkTTFWj5bsA1vKU8SDCyMuB
+RQV11DId5+RNNCmWnskORUZJQssvY49pnfCxCES2nt3l/XzTzVtLYmd6G9uAqVac
+e2ibnmDrFVlmlyRsCiMfZl5/OTJzt7Cj3az59m5Syfw/lnS9YP82t/r/ufuKkO5Q
+q5a9aI8DuNNmAjR4lpIJNqIpX/y+dG2aGmx4XTc31MR9szWtiTgOHe0MkMupOAL0
+qkHrBgwo1zjuTMf3QOg6Z5Q=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0y
+MjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vK
+EymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPan
+H05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jz
+kE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5B
+xxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uD
+JGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNV
+HSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1Ud
+IwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4E
+FgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/A
+Vn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTs
+Y4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48
+gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/
+TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMb
+SXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-1.3.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.3.cert
new file mode 100644
index 0000000000..0c947b17ba
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.3.cert
@@ -0,0 +1,124 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+MIIIHTCCBgWgAwIBAgIUCqrrzSfjzaoyB3DOxst2kMxFp/MwDQYJKoZIhvcNAQEL
+BQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyMTIyMjIy
+OFoXDTIxMDgyMTIyMzIwMFowgZsxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBv
+cmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MSYwJAYDVQQD
+DB1kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMPAPH2y206qios2NMzlCNJv1mrwC1/8tH2HOqJGiYZB
+O7QOBRSvJsV++IozCB8ap99e8B64OOAQPOyykrdXd2axhftmMb1SFMF56eukHSuz
+KhKWRUgHs0UFRU51lDcBcOvphwJ+5SOgqrqKFFFBgJ0ZpcP54JpFwKIdh3ac10x2
+mBaW5ccqdv5X9oEMu1D/yivBmy34tsbLYyfttCjP76iVT7UVYHjHWynnIhsEyMsU
+gdM90NzrTlrvTSi/EcCD1W3+8b0f+G1TI5rhHbKwR0n/mv5QLFm7EABoYPhxS8bX
+B+9tE67yb0RyWbgvUiHySRynQLNMRpRx8Y9bA8uC8n8CAwEAAaOCA6QwggOgMAkG
+A1UdEwQCMAAwHwYDVR0jBBgwFoAUsxKJtalLNbwVAPCA6dh4h/ETfHYwcwYIKwYB
+BQUHAQEEZzBlMDcGCCsGAQUFBzAChitodHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9i
+YWwuY29tL3F2c3NsZzMuY3J0MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5xdW92
+YWRpc2dsb2JhbC5jb20wgZ8GA1UdEQSBlzCBlIIdZGV2LmVuZXJneS5pbnNpZGUu
+dGVsc3RyYS5jb22CJXJlcG9ydHMuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5j
+b22CJ2dyZWVuc3luYy5kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbYIjbmdv
+c3MuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wUQYDVR0gBEowSDBGBgwr
+BgEEAb5YAAJkAQEwNjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5xdW92YWRpc2ds
+b2JhbC5jb20vcmVwb3NpdG9yeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwEwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybC5xdW92YWRpc2dsb2JhbC5j
+b20vcXZzc2xnMy5jcmwwHQYDVR0OBBYEFEoJQRpPC/V5ZK3mMkszZE2v6vh+MA4G
+A1UdDwEB/wQEAwIFoDCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFstk9Y+gAABAMARjBEAiBFMZa6
+O9iXVjy2kqQa54vgNFdU7shgFJJhm//fSAQZUAIgBIL/yPdh+XiuQS2xPhCzNYkh
+bxf7BbN4qUISESgiZpsAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZ
+EwAAAWy2T1nKAAAEAwBHMEUCIG0tp63jLsDsfCTDlcvV5ItjRkbUJBnkxlPdP2PH
+88sTAiEApgaPofVdn2hdI12iDDex72ta+9wpwQ1MxoaJn2nt+qEAdQDuS723dc5g
+uuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAWy2T1iJAAAEAwBGMEQCIE/mzEFp
+CJUc71jvwJa4Px86R3ZYK4mHmUlQAUZqd0ZkAiBdEmT8xxTuleSUlYHEkKCK/FZX
+L+vsYJpPrA9TsO5IsTANBgkqhkiG9w0BAQsFAAOCAgEApE9WLz3S8tqA9Dk3r9LF
+rJy8km9cBt1O9SQZwFsduGKGdF3Fd+/Y0V7UrFDzrX+NIzqcmgBHKxaIXorMBF70
+ajMaaROP2ymkpEXnruEwoR47fbW+JRAWDRm2xnouQveQX9ZcgCLbBvAWBqpndQj2
+DGmLJhNz5GlFBjh3PQZlU1w8hU7TrDxa7M1GMtVnk8X+o3l/MX9iPeEs+PiC4dHD
+hpj84RY1VQJz8+10rql47SB5YgbwcqaizTG4ax/OAv1JHNWtfAodIMX8Y8X00zoz
+A20LQv880jCCNANVNbrXJ3h4X3xwW/C1X9vYk0shymZJbT5u17JbPD1cy39bA7kT
+F4L7scdQRxvcqazYN4/IdgvgMji9OltiYufP88Ti8KB2tcl2accpiC5St/zllGD1
+hqEeYLMzjyvUKR/1uvURQQtc0DPvBRmvkB+aI4g+sLkTTFWj5bsA1vKU8SDCyMuB
+RQV11DId5+RNNCmWnskORUZJQssvY49pnfCxCES2nt3l/XzTzVtLYmd6G9uAqVac
+e2ibnmDrFVlmlyRsCiMfZl5/OTJzt7Cj3az59m5Syfw/lnS9YP82t/r/ufuKkO5Q
+q5a9aI8DuNNmAjR4lpIJNqIpX/y+dG2aGmx4XTc31MR9szWtiTgOHe0MkMupOAL0
+qkHrBgwo1zjuTMf3QOg6Z5Q=
+-----END CERTIFICATE-----
+
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
+
+
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0y
+MjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vK
+EymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPan
+H05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jz
+kE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5B
+xxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uD
+JGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNV
+HSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1Ud
+IwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4E
+FgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/A
+Vn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTs
+Y4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48
+gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/
+TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMb
+SXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-1.4.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.4.cert
new file mode 100644
index 0000000000..adbb8edcae
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-1.4.cert
@@ -0,0 +1,86 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+MIIIHTCCBgWgAwIBAgIUCqrrzSfjzaoyB3DOxst2kMxFp/MwDQYJKoZIhvcNAQEL
+BQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyMTIyMjIy
+OFoXDTIxMDgyMTIyMzIwMFowgZsxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBv
+cmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MSYwJAYDVQQD
+DB1kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAMPAPH2y206qios2NMzlCNJv1mrwC1/8tH2HOqJGiYZB
+O7QOBRSvJsV++IozCB8ap99e8B64OOAQPOyykrdXd2axhftmMb1SFMF56eukHSuz
+KhKWRUgHs0UFRU51lDcBcOvphwJ+5SOgqrqKFFFBgJ0ZpcP54JpFwKIdh3ac10x2
+mBaW5ccqdv5X9oEMu1D/yivBmy34tsbLYyfttCjP76iVT7UVYHjHWynnIhsEyMsU
+gdM90NzrTlrvTSi/EcCD1W3+8b0f+G1TI5rhHbKwR0n/mv5QLFm7EABoYPhxS8bX
+B+9tE67yb0RyWbgvUiHySRynQLNMRpRx8Y9bA8uC8n8CAwEAAaOCA6QwggOgMAkG
+A1UdEwQCMAAwHwYDVR0jBBgwFoAUsxKJtalLNbwVAPCA6dh4h/ETfHYwcwYIKwYB
+BQUHAQEEZzBlMDcGCCsGAQUFBzAChitodHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9i
+YWwuY29tL3F2c3NsZzMuY3J0MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5xdW92
+YWRpc2dsb2JhbC5jb20wgZ8GA1UdEQSBlzCBlIIdZGV2LmVuZXJneS5pbnNpZGUu
+dGVsc3RyYS5jb22CJXJlcG9ydHMuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5j
+b22CJ2dyZWVuc3luYy5kZXYuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbYIjbmdv
+c3MuZGV2LmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wUQYDVR0gBEowSDBGBgwr
+BgEEAb5YAAJkAQEwNjA0BggrBgEFBQcCARYoaHR0cDovL3d3dy5xdW92YWRpc2ds
+b2JhbC5jb20vcmVwb3NpdG9yeTAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
+AwEwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybC5xdW92YWRpc2dsb2JhbC5j
+b20vcXZzc2xnMy5jcmwwHQYDVR0OBBYEFEoJQRpPC/V5ZK3mMkszZE2v6vh+MA4G
+A1UdDwEB/wQEAwIFoDCCAXwGCisGAQQB1nkCBAIEggFsBIIBaAFmAHUAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFstk9Y+gAABAMARjBEAiBFMZa6
+O9iXVjy2kqQa54vgNFdU7shgFJJhm//fSAQZUAIgBIL/yPdh+XiuQS2xPhCzNYkh
+bxf7BbN4qUISESgiZpsAdgBvU3asMfAxGdiZAKRRFf93FRwR2QLBACkGjbIImjfZ
+EwAAAWy2T1nKAAAEAwBHMEUCIG0tp63jLsDsfCTDlcvV5ItjRkbUJBnkxlPdP2PH
+88sTAiEApgaPofVdn2hdI12iDDex72ta+9wpwQ1MxoaJn2nt+qEAdQDuS723dc5g
+uuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAWy2T1iJAAAEAwBGMEQCIE/mzEFp
+CJUc71jvwJa4Px86R3ZYK4mHmUlQAUZqd0ZkAiBdEmT8xxTuleSUlYHEkKCK/FZX
+L+vsYJpPrA9TsO5IsTANBgkqhkiG9w0BAQsFAAOCAgEApE9WLz3S8tqA9Dk3r9LF
+rJy8km9cBt1O9SQZwFsduGKGdF3Fd+/Y0V7UrFDzrX+NIzqcmgBHKxaIXorMBF70
+ajMaaROP2ymkpEXnruEwoR47fbW+JRAWDRm2xnouQveQX9ZcgCLbBvAWBqpndQj2
+DGmLJhNz5GlFBjh3PQZlU1w8hU7TrDxa7M1GMtVnk8X+o3l/MX9iPeEs+PiC4dHD
+hpj84RY1VQJz8+10rql47SB5YgbwcqaizTG4ax/OAv1JHNWtfAodIMX8Y8X00zoz
+A20LQv880jCCNANVNbrXJ3h4X3xwW/C1X9vYk0shymZJbT5u17JbPD1cy39bA7kT
+F4L7scdQRxvcqazYN4/IdgvgMji9OltiYufP88Ti8KB2tcl2accpiC5St/zllGD1
+hqEeYLMzjyvUKR/1uvURQQtc0DPvBRmvkB+aI4g+sLkTTFWj5bsA1vKU8SDCyMuB
+RQV11DId5+RNNCmWnskORUZJQssvY49pnfCxCES2nt3l/XzTzVtLYmd6G9uAqVac
+e2ibnmDrFVlmlyRsCiMfZl5/OTJzt7Cj3az59m5Syfw/lnS9YP82t/r/ufuKkO5Q
+q5a9aI8DuNNmAjR4lpIJNqIpX/y+dG2aGmx4XTc31MR9szWtiTgOHe0MkMupOAL0
+qkHrBgwo1zjuTMf3QOg6Z5Q=
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0y
+MjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vK
+EymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPan
+H05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jz
+kE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5B
+xxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uD
+JGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNV
+HSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1Ud
+IwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4E
+FgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/A
+Vn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTs
+Y4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48
+gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/
+TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMb
+SXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/chain-4.cert b/test/units/modules/cloud/amazon/fixtures/certs/chain-4.cert
new file mode 100644
index 0000000000..2b82edf6c9
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/chain-4.cert
@@ -0,0 +1,121 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=prod.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+MIIIJDCCBgygAwIBAgIUP9S/56XvOFzWk1vp1+7JJT17brEwDQYJKoZIhvcNAQEL
+BQAwTTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxIzAh
+BgNVBAMTGlF1b1ZhZGlzIEdsb2JhbCBTU0wgSUNBIEczMB4XDTE5MDgyNzAzMTU1
+NFoXDTIxMDgyNzAzMjUwMFowgZwxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0
+b3JpYTESMBAGA1UEBwwJTWVsYm91cm5lMSQwIgYDVQQKDBtUZWxzdHJhIENvcnBv
+cmF0aW9uIExpbWl0ZWQxFzAVBgNVBAsMDlRlbHN0cmEgRW5lcmd5MScwJQYDVQQD
+DB5wcm9kLmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb20wggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCrRouNZFOZwM1qyAU6v6ag9fzSx3y8zz36nR8HuqbA
+/wqrbMmnpofwdx/9u1bilsHfJzIODv0hm7aGk+neTK3DIapiII3m0HKW0v+GLsl7
+JkDuc2o3XlakcXlA45qDKCZXbXZtY4/kdxKG0OSUZi7oQqohhYl/c/ojrTiey+4G
+KhEVqWwOuQ1OC1DRw4qMH54d0koFxxSLPJ8JiiztLlK/e9n8BoJikj5fBqWy5R1F
+bGXCdzjcfmPV6iSOzJShpUgj4ga91mO6j3S6LLfK5ibbTlY+pmUxUT+m9nKMon3h
+mFptTYo9t9vUF/a/owjRxNLg01fJLNjYn8QV2vQvODGfAgMBAAGjggOqMIIDpjAJ
+BgNVHRMEAjAAMB8GA1UdIwQYMBaAFLMSibWpSzW8FQDwgOnYeIfxE3x2MHMGCCsG
+AQUFBwEBBGcwZTA3BggrBgEFBQcwAoYraHR0cDovL3RydXN0LnF1b3ZhZGlzZ2xv
+YmFsLmNvbS9xdnNzbGczLmNydDAqBggrBgEFBQcwAYYeaHR0cDovL29jc3AucXVv
+dmFkaXNnbG9iYWwuY29tMIGjBgNVHREEgZswgZiCHnByb2QuZW5lcmd5Lmluc2lk
+ZS50ZWxzdHJhLmNvbYImcmVwb3J0cy5wcm9kLmVuZXJneS5pbnNpZGUudGVsc3Ry
+YS5jb22CKGdyZWVuc3luYy5wcm9kLmVuZXJneS5pbnNpZGUudGVsc3RyYS5jb22C
+JG5nb3NzLnByb2QuZW5lcmd5Lmluc2lkZS50ZWxzdHJhLmNvbTBRBgNVHSAESjBI
+MEYGDCsGAQQBvlgAAmQBATA2MDQGCCsGAQUFBwIBFihodHRwOi8vd3d3LnF1b3Zh
+ZGlzZ2xvYmFsLmNvbS9yZXBvc2l0b3J5MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr
+BgEFBQcDATA6BgNVHR8EMzAxMC+gLaArhilodHRwOi8vY3JsLnF1b3ZhZGlzZ2xv
+YmFsLmNvbS9xdnNzbGczLmNybDAdBgNVHQ4EFgQUoIME5TykVAI8VF5g0zeh0xdv
+i3owDgYDVR0PAQH/BAQDAgWgMIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgBW
+FAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWzRG8r0AAAEAwBHMEUC
+IQDShuQyYMiy7KKxWOzffolVIcPRgWD7ClNEbIcUATHKyQIgXnTZBXcpcbXBQXLs
+tFuvY36TbKIYc2ql2nmdydGQ9wcAdgCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jj
+d80OyA3cEAAAAWzRG8sAAAAEAwBHMEUCIGsLEoA9S7pNE3VoNZHxl2IAdeP3Dy2Q
+Mk0rM46hp6CRAiEA08rOjswSdcn7qgDEoiyvlcrOTIFJAEcMlxSY65yLVUwAdgBV
+gdTCFpA2AUrqC5tXPFPwwOQ4eHAlCBcvo6odBxPTDAAAAWzRG8q7AAAEAwBHMEUC
+IAkVCcTFG8MBDI58JKIhMlPbzkdrKnYY3Kp9KqWuTAvMAiEAipeI7RCLBk8+T/p+
+gY7+vtFZxKDthcJMUpZz7qmica0wDQYJKoZIhvcNAQELBQADggIBAESe0U1qArxL
+F2uk65q6x6HBcZuSocpceokzcUBv07Kxs6UJU9ybTbl8VYPuC+OUdpvut1kOJCJm
+1TRrr5KMh+9as42xkbKRZnh5TQt7aHmVcLHLfA4x0UrELfNX3fVTDxwDAPAhE5oM
+0w+d1foLakh7dXKKSxobEI3KRwFp19iuZeIqwI8XMWMr9ajhTC0T7D2QvKotpNBS
+sNDHiIE3IXoa9o7UiOG8IfW0wAt7CEygv0F7ctHRTcQSP/SJIGYOUZ7uotULVL5i
+elG31Y83Jx3sPNCy4IZfCip6Gw7MgsN2CZGApqi49edSqDWyRIfmCeXtMc7XI7Md
+kqqWxbqGGTdYJCucoGqahqRR+BI9anEqTD9T5Gy0TpCi2pgp1i7czza71nfz0PcN
+R0pw/1lqb9AqmJ2XELpBpo82B9XGple9thpincai7jPk3ezY5eEvDTmkHRlUFCp8
+8M66Ga19hZTgnHPWDKZYZzuZ7Lcl2WbapFOYYHJggSpBRy4GkH6eTSkUB9G9k8vU
+gbvtS7sR5ggecbCBu0M4TWYmnUojR8UXtr0oOTlXysTHVGs5Tx9ChhOLyUqhX8tM
+1zSDT8JJvbbw4RqpGzBKTNaO5nxRLgKVQOQdM8f1kjMr9/U58Lc4UiaTkJM14VfK
+8GfV8+K/vRCBtME53ILvm1l18jtakG3c
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIGFzCCA/+gAwIBAgIUftbnnMmtgcTIGT75XUQodw40ExcwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjExMDYxNDUwMThaFw0y
+MjExMDYxNDUwMThaME0xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMSMwIQYDVQQDExpRdW9WYWRpcyBHbG9iYWwgU1NMIElDQSBHMzCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANf8Od17be6c6lTGJDhEXpmkTs4y
+Q39Rr5VJyBeWCg06nSS71s6xF3sZvKcV0MbXlXCYM2ZX7cNTbJ81gs7uDsKFp+vK
+EymiKyEiI2SImOtECNnSg+RVR4np/xz/UlC0yFUisH75cZsJ8T1pkGMfiEouR0EM
+7O0uFgoboRfUP582TTWy0F7ynSA6YfGKnKj0OFwZJmGHVkLs1VevWjhj3R1fsPan
+H05P5moePFnpQdj1FofoSxUHZ0c7VB+sUimboHm/uHNY1LOsk77qiSuVC5/yrdg3
+2EEfP/mxJYT4r/5UiD7VahySzeZHzZ2OibQm2AfgfMN3l57lCM3/WPQBhMAPS1jz
+kE+7MjajM2f0aZctimW4Hasrj8AQnfAdHqZehbhtXaAlffNEzCdpNK584oCTVR7N
+UR9iZFx83ruTqpo+GcLP/iSYqhM4g7fy45sNhU+IS+ca03zbxTl3TTlkofXunI5B
+xxE30eGSQpDZ5+iUJcEOAuVKrlYocFbB3KF45hwcbzPWQ1DcO2jFAapOtQzeS+MZ
+yZzT2YseJ8hQHKu8YrXZWwKaNfyl8kFkHUBDICowNEoZvBwRCQp8sgqL6YRZy0uD
+JGxmnC2e0BVKSjcIvmq/CRWH7yiTk9eWm73xrsg9iIyD/kwJEnLyIk8tR5V8p/hc
+1H2AjDrZH12PsZ45AgMBAAGjgfMwgfAwEgYDVR0TAQH/BAgwBgEB/wIBATARBgNV
+HSAECjAIMAYGBFUdIAAwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRw
+Oi8vb2NzcC5xdW92YWRpc2dsb2JhbC5jb20wDgYDVR0PAQH/BAQDAgEGMB8GA1Ud
+IwQYMBaAFO3nb3Zav2DsSVvGpXe7chZxm8Q9MDsGA1UdHwQ0MDIwMKAuoCyGKmh0
+dHA6Ly9jcmwucXVvdmFkaXNnbG9iYWwuY29tL3F2cmNhMmczLmNybDAdBgNVHQ4E
+FgQUsxKJtalLNbwVAPCA6dh4h/ETfHYwDQYJKoZIhvcNAQELBQADggIBAFGm1Fqp
+RMiKr7a6h707M+km36PVXZnX1NZocCn36MrfRvphotbOCDm+GmRkar9ZMGhc8c/A
+Vn7JSCjwF9jNOFIOUyNLq0w4luk+Pt2YFDbgF8IDdx53xIo8Gv05e9xpTvQYaIto
+qeHbQjGXfSGc91olfX6JUwZlxxbhdJH+rxTFAg0jcbqToJoScWTfXSr1QRcNbSTs
+Y4CPG6oULsnhVvrzgldGSK+DxFi2OKcDsOKkV7W4IGg8Do2L/M588AfBnV8ERzpl
+qgMBBQxC2+0N6RdFHbmZt0HQE/NIg1s0xcjGx1XW3YTOfje31rmAXKHOehm4Bu48
+gr8gePq5cdQ2W9tA0Dnytb9wzH2SyPPIXRI7yNxaX9H8wYeDeeiKSSmQtfh1v5cV
+7RXvm8F6hLJkkco/HOW3dAUwZFcKsUH+1eUJKLN18eDGwB8yGawjHvOKqcfg5Lf/
+TvC7hgcx7pDYaCCaqHaekgUwXbB2Enzqr1fdwoU1c01W5YuQAtAx5wk1bf34Yq/J
+ph7wNXGvo88N0/EfP9AdVGmJzy7VuRXeVAOyjKAIeADMlwpjBRhcbs9m3dkqvoMb
+SXKJxv/hFmNgEOvOlaFsXX1dbKg1v+C1AzKAFdiuAIa62JzASiEhigqNSdqdTsOh
+8W8hdONuKKpe9zKedhBFAvuxhDgKmnySglYc
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
+BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
+BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
+MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
+aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
+SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
+qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
+n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
+c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
+o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
+IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
+IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
+8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
+vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
+7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
+cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
+BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
+ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
+roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
+W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
+lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
+csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
+dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
+KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
+HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
+WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-a.cert b/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-a.cert
new file mode 100644
index 0000000000..1d9bbe2137
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-a.cert
@@ -0,0 +1,18 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+aaa
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+bbb
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+ccc
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-b.cert b/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-b.cert
new file mode 100644
index 0000000000..1d9bbe2137
--- /dev/null
+++ b/test/units/modules/cloud/amazon/fixtures/certs/simple-chain-b.cert
@@ -0,0 +1,18 @@
+subject=/C=AU/ST=Victoria/L=Melbourne/O=Telstra Corporation Limited/OU=Telstra Energy/CN=dev.energy.inside.telstra.com
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+-----BEGIN CERTIFICATE-----
+aaa
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Global SSL ICA G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+bbb
+-----END CERTIFICATE-----
+
+subject=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+issuer=/C=BM/O=QuoVadis Limited/CN=QuoVadis Root CA 2 G3
+-----BEGIN CERTIFICATE-----
+ccc
+-----END CERTIFICATE-----
+
diff --git a/test/units/modules/cloud/amazon/test_aws_acm.py b/test/units/modules/cloud/amazon/test_aws_acm.py
new file mode 100644
index 0000000000..d2fd87b8de
--- /dev/null
+++ b/test/units/modules/cloud/amazon/test_aws_acm.py
@@ -0,0 +1,122 @@
+# (c) 2019 Telstra Corporation Limited
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+from ansible.modules.cloud.amazon.aws_acm import pem_chain_split, chain_compare
+from ansible.module_utils._text import to_bytes, to_text
+from pprint import pprint
+
+
+def test_chain_compare():
+
+ # The functions we're testing take module as an argument
+ # Just so they can call module.fail_json
+ # Let's just use None for the unit tests,
+ # Because they shouldn't fail
+ # And if they do, fail_json is not applicable
+ module = None
+
+ fixture_suffix = 'test/units/modules/cloud/amazon/fixtures/certs'
+
+ # Test chain split function on super simple (invalid) certs
+ expected = ['aaa', 'bbb', 'ccc']
+
+ for fname in ['simple-chain-a.cert', 'simple-chain-b.cert']:
+ path = fixture_suffix + '/' + fname
+ with open(path, 'r') as f:
+ pem = to_text(f.read())
+ actual = pem_chain_split(module, pem)
+ actual = [a.strip() for a in actual]
+ if actual != expected:
+ print("Expected:")
+ pprint(expected)
+ print("Actual:")
+ pprint(actual)
+ raise AssertionError("Failed to properly split %s" % fname)
+
+ # Now test real chains
+ # chains with same same_as should be considered equal
+ test_chains = [
+ { # Original Cert chain
+ 'path': fixture_suffix + '/chain-1.0.cert',
+ 'same_as': 1,
+ 'length': 3
+ },
+ { # Same as 1.0, but longer PEM lines
+ 'path': fixture_suffix + '/chain-1.1.cert',
+ 'same_as': 1,
+ 'length': 3
+ },
+ { # Same as 1.0, but without the stuff before each --------
+ 'path': fixture_suffix + '/chain-1.2.cert',
+ 'same_as': 1,
+ 'length': 3
+ },
+ { # Same as 1.0, but in a different order, so should be considered different
+ 'path': fixture_suffix + '/chain-1.3.cert',
+ 'same_as': 2,
+ 'length': 3
+ },
+ { # Same as 1.0, but with last link missing
+ 'path': fixture_suffix + '/chain-1.4.cert',
+ 'same_as': 3,
+ 'length': 2
+ },
+ { # Completely different cert chain to all the others
+ 'path': fixture_suffix + '/chain-4.cert',
+ 'same_as': 4,
+ 'length': 3
+ },
+ { # Single cert
+ 'path': fixture_suffix + '/a.pem',
+ 'same_as': 5,
+ 'length': 1
+ },
+ { # a different, single cert
+ 'path': fixture_suffix + '/b.pem',
+ 'same_as': 6,
+ 'length': 1
+ }
+ ]
+
+ for chain in test_chains:
+ with open(chain['path'], 'r') as f:
+ chain['pem_text'] = to_text(f.read())
+
+ # Test to make sure our regex isn't too greedy
+ chain['split'] = pem_chain_split(module, chain['pem_text'])
+ if len(chain['split']) != chain['length']:
+ print("Cert before split")
+ print(chain['pem_text'])
+ print("Cert after split")
+ pprint(chain['split'])
+ print("path: %s" % chain['path'])
+ print("Expected chain length: %d" % chain['length'])
+ print("Actual chain length: %d" % len(chain['split']))
+ raise AssertionError("Chain %s was not split properly" % chain['path'])
+
+ for chain_a in test_chains:
+ for chain_b in test_chains:
+ expected = (chain_a['same_as'] == chain_b['same_as'])
+
+ # Now test the comparison function
+ actual = chain_compare(module, chain_a['pem_text'], chain_b['pem_text'])
+ if expected != actual:
+ print("Error, unexpected comparison result between \n%s\nand\n%s" % (chain_a['path'], chain_b['path']))
+ print("Expected %s got %s" % (str(expected), str(actual)))
+ assert(expected == actual)