acme_certificate and acme_certificate_create_order: add order_creation_error_strategy and order_creation_max_retries options (#842)

* Provide error information.

* Add helper function for order creation retrying.

* Improve existing documentation.

* Document 'replaces' return value.

* Add order_creation_error_strategy and order_creation_max_retries options.

* Add changelog fragment.

* Fix authz deactivation for finalizing step.

* Fix profile handling on order creation.

* Improve existing tests.

* Add ARI and profile tests.

* Warn when 'replaces' is removed when retrying to create an order.
pull/844/head
Felix Fontein 2025-01-18 10:51:10 +01:00 committed by GitHub
parent b9fa5b5193
commit 214794d056
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 632 additions and 56 deletions

View File

@ -0,0 +1,6 @@
minor_changes:
- "acme_certificate - add options ``order_creation_error_strategy`` and ``order_creation_max_retries``
which allow to configure the error handling behavior if creating a new ACME order fails. This is
particularly important when using the ``include_renewal_cert_id`` option, and the default value
``auto`` for ``order_creation_error_strategy`` tries to gracefully handle related errors
(https://github.com/ansible-collections/community.crypto/pull/842)."

View File

@ -19,6 +19,7 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.account impo
)
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
Authorization,
wait_for_validation,
)
@ -63,6 +64,8 @@ class ACMECertificateClient(object):
account = ACMEAccount(self.client)
self.account = account
self.order_uri = module.params.get('order_uri')
self.order_creation_error_strategy = module.params.get('order_creation_error_strategy', 'auto')
self.order_creation_max_retries = module.params.get('order_creation_max_retries', 3)
# Make sure account exists
dummy, account_data = self.account.setup_account(allow_creation=False)
@ -102,7 +105,15 @@ class ACMECertificateClient(object):
'''
if self.identifiers is None:
raise ModuleFailException('No identifiers have been provided')
order = Order.create(self.client, self.identifiers, replaces_cert_id=replaces_cert_id, profile=profile)
order = Order.create_with_error_handling(
self.client,
self.identifiers,
error_strategy=self.order_creation_error_strategy,
error_max_retries=self.order_creation_max_retries,
replaces_cert_id=replaces_cert_id,
profile=profile,
message_callback=self.module.warn,
)
self.order_uri = order.url
order.load_authorizations(self.client)
return order
@ -248,6 +259,17 @@ class ACMECertificateClient(object):
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
'''
if len(order.authorization_uris) > len(order.authorizations):
for authz_uri in order.authorization_uris:
authz = None
try:
authz = Authorization.deactivate_url(self.client, authz_uri)
except Exception:
# ignore errors
pass
if authz is None or authz.status != 'deactivated':
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz_uri))
else:
for authz in order.authorizations.values():
try:
authz.deactivate(self.client)

View File

@ -322,6 +322,23 @@ class Authorization(object):
return True
return False
@classmethod
def deactivate_url(cls, client, url):
'''
Deactivates this authorization.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
'''
authz = cls(url)
authz_deactivate = {
'status': 'deactivated'
}
if client.version == 1:
authz_deactivate['resource'] = 'authz'
result, info = client.send_signed_request(url, authz_deactivate, fail_on_error=True)
authz._setup(client, result)
return authz
def wait_for_validation(authzs, client):
'''

View File

@ -84,6 +84,8 @@ class ACMEProtocolException(ModuleFailException):
pass
extras = extras or dict()
error_code = None
error_type = None
if msg is None:
msg = 'ACME request failed'
@ -94,7 +96,9 @@ class ACMEProtocolException(ModuleFailException):
code = info['status']
extras['http_url'] = url
extras['http_status'] = code
error_code = code
if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
error_type = content_json['type']
if 'status' in content_json and content_json['status'] != code:
code_msg = 'status {problem_code} (HTTP status: {http_code})'.format(
http_code=format_http_status(code), problem_code=content_json['status'])
@ -134,6 +138,8 @@ class ACMEProtocolException(ModuleFailException):
)
self.problem = {}
self.subproblems = []
self.error_code = error_code
self.error_type = error_type
for k, v in extras.items():
setattr(self, k, v)

View File

@ -87,6 +87,55 @@ class Order(object):
client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201])
return cls.from_json(client, result, info['location'])
@classmethod
def create_with_error_handling(
cls,
client,
identifiers,
error_strategy='auto',
error_max_retries=3,
replaces_cert_id=None,
profile=None,
message_callback=None,
):
"""
error_strategy can be one of the following strings:
* ``fail``: simply fail. (Same behavior as ``Order.create()``.)
* ``retry_without_replaces_cert_id``: if ``replaces_cert_id`` is not ``None``, set it to ``None`` and retry.
The only exception is an error of type ``urn:ietf:params:acme:error:alreadyReplaced``, that indicates that
the certificate was already replaced.
* ``auto``: try to be clever. Right now this is identical to ``retry_without_replaces_cert_id``, but that can
change at any time in the future.
* ``always``: always retry until ``error_max_retries`` has been reached.
"""
tries = 0
while True:
tries += 1
try:
return cls.create(client, identifiers, replaces_cert_id=replaces_cert_id, profile=profile)
except ACMEProtocolException as exc:
if tries <= error_max_retries + 1 and error_strategy != 'fail':
if error_strategy == 'always':
continue
if (
error_strategy in ('auto', 'retry_without_replaces_cert_id') and
replaces_cert_id is not None and
not (exc.error_code == 409 and exc.error_type == 'urn:ietf:params:acme:error:alreadyReplaced')
):
replaces_cert_id = None
if message_callback:
message_callback(
'Stop passing `replaces` due to error {code} {type} when creating ACME order'.format(
code=exc.error_code,
type=exc.error_type,
)
)
continue
raise
def refresh(self, client):
result, dummy = client.get_request(self.url)
changed = self.data != result

View File

@ -243,8 +243,6 @@ options:
- Determines whether to request renewal of an existing certificate according to L(the ACME ARI draft 3,
https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5).
- This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists.
- V(never) never sends the certificate ID of the certificate to renew. V(always) will always send it.
- V(when_ari_supported) only sends the certificate ID if the ARI endpoint is found in the ACME directory.
- Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible draft (or final
version, once it is out) of the ARI extension. V(always) should never be necessary. If you are not sure, or if you
receive strange errors on invalid C(replaces) values in order objects, use V(never), which also happens to be the
@ -252,15 +250,19 @@ options:
- ACME servers might refuse to create new orders with C(replaces) for certificates that already have an existing order.
This can happen if this module is used to create an order, and then the playbook/role fails in case the challenges
cannot be set up. If the playbook/role does not record the order data to continue with the existing order, but tries
to create a new one on the next run, creating the new order might fail. For this reason, this option should only be
set to a value different from V(never) if the role/playbook using it keeps track of order data accross restarts, or
if it takes care to deactivate orders whose processing is aborted. Orders can be deactivated with the
to create a new one on the next run, creating the new order might fail. If O(order_creation_error_strategy=fail)
this will make the module fail. O(order_creation_error_strategy=auto) and
O(order_creation_error_strategy=retry_without_replaces_cert_id) will avoid this by leaving away C(replaces)
on retries.
- If O(order_creation_error_strategy=fail), for the above reason, this option should only be set to a value different
from V(never) if the role/playbook using it keeps track of order data accross restarts, or if it takes care to
deactivate orders whose processing is aborted. Orders can be deactivated with the
M(community.crypto.acme_certificate_deactivate_authz) module.
type: str
choices:
- never
- when_ari_supported
- always
never: Never send the certificate ID of the certificate to renew.
when_ari_supported: Only send the certificate ID if the ARI endpoint is found in the ACME directory.
always: Will always send the certificate ID of the certificate to renew.
default: never
version_added: 2.20.0
profile:
@ -271,6 +273,32 @@ options:
for more information.
type: str
version_added: 2.24.0
order_creation_error_strategy:
description:
- Selects the error handling strategy for ACME protocol errors if creating a new ACME order fails.
type: str
choices:
auto:
- An unspecified algorithm that tries to be clever.
- Right now identical to V(retry_without_replaces_cert_id).
always:
- Always retry, until the limit in O(order_creation_max_retries) has been reached.
fail:
- Simply fail in case of errors. Do not attempt to retry.
- This has been the default before community.crypto 2.24.0.
retry_without_replaces_cert_id:
- If O(include_renewal_cert_id) is present, creating the order will be tried again without C(replaces).
- The only exception is an error of type C(urn:ietf:params:acme:error:alreadyReplaced), that indicates that
the certificate was already replaced. This usually means something went wrong and the user should investigate.
default: auto
version_added: 2.24.0
order_creation_max_retries:
description:
- Depending on the strategy selected in O(order_creation_error_strategy), will retry creating new orders
for at most the specified amount of times.
type: int
default: 3
version_added: 2.24.0
"""
EXAMPLES = r"""
@ -613,6 +641,8 @@ class ACMECertificateClient(object):
self.select_chain_matcher = []
self.include_renewal_cert_id = module.params['include_renewal_cert_id']
self.profile = module.params['profile']
self.order_creation_error_strategy = module.params['order_creation_error_strategy']
self.order_creation_max_retries = module.params['order_creation_max_retries']
if self.module.params['select_chain']:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
@ -712,7 +742,15 @@ class ACMECertificateClient(object):
cert_info=cert_info,
none_if_required_information_is_missing=True,
)
self.order = Order.create(self.client, self.identifiers, replaces_cert_id, profile=self.profile)
self.order = Order.create_with_error_handling(
self.client,
self.identifiers,
error_strategy=self.order_creation_error_strategy,
error_max_retries=self.order_creation_max_retries,
replaces_cert_id=replaces_cert_id,
profile=self.profile,
message_callback=self.module.warn,
)
self.order_uri = self.order.url
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
@ -899,6 +937,8 @@ def main():
)),
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
profile=dict(type='str'),
order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']),
order_creation_max_retries=dict(type='int', default=3),
)
argument_spec.update(
required_one_of=[

View File

@ -113,15 +113,18 @@ options:
according to L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5).
- This certificate ID must be computed as specified in
L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.1).
It is returned as RV(community.crypto.acme_certificate_renewal_info#module:cert_id) of the
It is returned as return value RV(community.crypto.acme_certificate_renewal_info#module:cert_id) of the
M(community.crypto.acme_certificate_renewal_info) module.
- ACME servers might refuse to create new orders that indicate to replace a certificate for which
an active replacement order already exists. This can happen if this module is used to create an order,
and then the playbook/role fails in case the challenges cannot be set up. If the playbook/role does not
record the order data to continue with the existing order, but tries to create a new one on the next run,
creating the new order might fail. For this reason, this option should only be used if the role/playbook
using it keeps track of order data accross restarts, or if it takes care to deactivate orders whose
processing is aborted. Orders can be deactivated with the
creating the new order might fail. If O(order_creation_error_strategy=fail) this will make the module fail.
O(order_creation_error_strategy=auto) and O(order_creation_error_strategy=retry_without_replaces_cert_id)
will avoid this by leaving away C(replaces) on retries.
- If O(order_creation_error_strategy=fail), for the above reason, this option should only be used
if the role/playbook using it keeps track of order data accross restarts, or if it takes care to
deactivate orders whose processing is aborted. Orders can be deactivated with the
M(community.crypto.acme_certificate_deactivate_authz) module.
type: str
profile:
@ -131,6 +134,29 @@ options:
L(draft-aaron-acme-profiles-00, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
for more information.
type: str
order_creation_error_strategy:
description:
- Selects the error handling strategy for ACME protocol errors if creating a new ACME order fails.
type: str
choices:
auto:
- An unspecified algorithm that tries to be clever.
- Right now identical to V(retry_without_replaces_cert_id).
always:
- Always retry, until the limit in O(order_creation_max_retries) has been reached.
fail:
- Simply fail in case of errors. Do not attempt to retry.
retry_without_replaces_cert_id:
- If O(replaces_cert_id) is present, creating the order will be tried again without C(replaces).
- The only exception is an error of type C(urn:ietf:params:acme:error:alreadyReplaced), that indicates that
the certificate was already replaced. This usually means something went wrong and the user should investigate.
default: auto
order_creation_max_retries:
description:
- Depending on the strategy selected in O(order_creation_error_strategy), will retry creating new orders
for at most the specified amount of times.
type: int
default: 3
'''
EXAMPLES = r'''
@ -370,6 +396,8 @@ def main():
deactivate_authzs=dict(type='bool', default=True),
replaces_cert_id=dict(type='str'),
profile=dict(type='str'),
order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']),
order_creation_max_retries=dict(type='int', default=3),
)
module = argument_spec.create_ansible_module()
if module.params['acme_version'] == 1:
@ -382,7 +410,7 @@ def main():
profile = module.params['profile']
if profile is not None:
meta_profiles = (client.directory.get('meta') or {}).get('profiles') or {}
meta_profiles = (client.client.directory.get('meta') or {}).get('profiles') or {}
if not meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support profiles. Please omit the "profile" option.')
if profile not in meta_profiles:

View File

@ -175,6 +175,13 @@ order:
- A URL for the certificate that has been issued in response to this order.
type: str
returned: when the certificate has been issued
replaces:
description:
- If the order was created to replace an existing certificate using the C(replaces) mechanism from
L(draft-ietf-acme-ari, https://datatracker.ietf.org/doc/draft-ietf-acme-ari/), this provides the
certificate ID of the certificate that will be replaced by this order.
type: str
returned: when the certificate order is replacing a certificate through draft-ietf-acme-ari
authorizations_by_identifier:
description:
- A dictionary mapping identifiers to their authorization objects.

View File

@ -3,23 +3,23 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Generate random domain name
- name: "({{ select_crypto_backend }}) Generate random domain name"
set_fact:
domain_name: "host{{ '%0x' % ((2**32) | random) }}.example.com"
- name: Generate account key
- name: "({{ select_crypto_backend }}) Generate account key"
openssl_privatekey:
path: "{{ remote_tmp_dir }}/accountkey.pem"
type: ECC
curve: secp256r1
force: true
- name: Parse account keys (to ease debugging some test failures)
- name: "({{ select_crypto_backend }}) Parse account keys (to ease debugging some test failures)"
openssl_privatekey_info:
path: "{{ remote_tmp_dir }}/accountkey.pem"
return_private_key_data: true
- name: Create ACME account
- name: "({{ select_crypto_backend }}) Create ACME account"
acme_account:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -30,14 +30,14 @@
state: present
register: account
- name: Generate certificate key
- name: "({{ select_crypto_backend }}) Generate certificate key"
openssl_privatekey:
path: "{{ remote_tmp_dir }}/cert.key"
type: ECC
curve: secp256r1
force: true
- name: Generate certificate CSR
- name: "({{ select_crypto_backend }}) Generate certificate CSR"
openssl_csr:
path: "{{ remote_tmp_dir }}/cert.csr"
privatekey_path: "{{ remote_tmp_dir }}/cert.key"
@ -46,7 +46,7 @@
return_content: true
register: csr
- name: Create certificate order
- name: "({{ select_crypto_backend }}) Create certificate order"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -56,11 +56,11 @@
csr: "{{ remote_tmp_dir }}/cert.csr"
register: order
- name: Show order information
- name: "({{ select_crypto_backend }}) Show order information"
debug:
var: order
- name: Check order
- name: "({{ select_crypto_backend }}) Check order"
assert:
that:
- order is changed
@ -80,7 +80,7 @@
- order.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1
- order.account_uri == account.account_uri
- name: Get order information
- name: "({{ select_crypto_backend }}) Get order information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -90,11 +90,11 @@
order_uri: "{{ order.order_uri }}"
register: order_info_1
- name: Show order information
- name: "({{ select_crypto_backend }}) Show order information"
debug:
var: order_info_1
- name: Check order information
- name: "({{ select_crypto_backend }}) Check order information"
assert:
that:
- order_info_1 is not changed
@ -115,10 +115,11 @@
- order_info_1.order.authorizations[0] == order_info_1.authorizations_by_identifier['dns:' ~ domain_name].uri
- "'certificate' not in order_info_1.order"
- order_info_1.order.status == 'pending'
- order_info_1.order.replaces is not defined
- order_info_1.order_uri == order.order_uri
- order_info_1.account_uri == account.account_uri
- name: Create HTTP challenges
- name: "({{ select_crypto_backend }}) Create HTTP challenges"
uri:
url: "http://{{ acme_host }}:5000/http/{{ item.identifier }}/{{ item.challenges['http-01'].resource[('.well-known/acme-challenge/'|length):] }}"
method: PUT
@ -129,7 +130,7 @@
loop: "{{ order.challenge_data }}"
when: "'http-01' in item.challenges"
- name: Let the challenge be validated
- name: "({{ select_crypto_backend }}) Let the challenge be validated"
community.crypto.acme_certificate_order_validate:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -140,18 +141,18 @@
challenge: http-01
register: validate_1
- name: Check validation result
- name: "({{ select_crypto_backend }}) Check validation result"
assert:
that:
- validate_1 is changed
- validate_1.account_uri == account.account_uri
- name: Wait until we know that the challenges have been validated for ansible-core <= 2.11
- name: "({{ select_crypto_backend }}) Wait until we know that the challenges have been validated for ansible-core <= 2.11"
pause:
seconds: 5
when: ansible_version.full is version('2.12', '<')
- name: Get order information
- name: "({{ select_crypto_backend }}) Get order information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -161,11 +162,11 @@
order_uri: "{{ order.order_uri }}"
register: order_info_2
- name: Show order information
- name: "({{ select_crypto_backend }}) Show order information"
debug:
var: order_info_2
- name: Check order information
- name: "({{ select_crypto_backend }}) Check order information"
assert:
that:
- order_info_2 is not changed
@ -186,10 +187,11 @@
- order_info_2.order.authorizations[0] == order_info_2.authorizations_by_identifier['dns:' ~ domain_name].uri
- "'certificate' not in order_info_2.order"
- order_info_2.order.status in ['pending', 'ready']
- order_info_2.order.replaces is not defined
- order_info_2.order_uri == order.order_uri
- order_info_2.account_uri == account.account_uri
- name: Let the challenge be validated (idempotent)
- name: "({{ select_crypto_backend }}) Let the challenge be validated (idempotent)"
community.crypto.acme_certificate_order_validate:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -200,13 +202,13 @@
challenge: http-01
register: validate_2
- name: Check validation result
- name: "({{ select_crypto_backend }}) Check validation result"
assert:
that:
- validate_2 is not changed
- validate_2.account_uri == account.account_uri
- name: Retrieve the cert and intermediate certificate
- name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate"
community.crypto.acme_certificate_order_finalize:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -214,6 +216,7 @@
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ order.order_uri }}"
deactivate_authzs: never
retrieve_all_alternates: true
csr: "{{ remote_tmp_dir }}/cert.csr"
cert_dest: "{{ remote_tmp_dir }}/cert.pem"
@ -221,7 +224,7 @@
fullchain_dest: "{{ remote_tmp_dir }}/cert-fullchain.pem"
register: finalize_1
- name: Check finalization result
- name: "({{ select_crypto_backend }}) Check finalization result"
assert:
that:
- finalize_1 is changed
@ -232,7 +235,7 @@
- finalize_1.selected_chain.chain.startswith('-----BEGIN CERTIFICATE-----\nMII')
- finalize_1.selected_chain.full_chain == finalize_1.selected_chain.cert + finalize_1.selected_chain.chain
- name: Read files from disk
- name: "({{ select_crypto_backend }}) Read files from disk"
slurp:
src: "{{ remote_tmp_dir }}/{{ item }}.pem"
loop:
@ -241,14 +244,14 @@
- cert-fullchain
register: slurp
- name: Compare finalization result with files on disk
- name: "({{ select_crypto_backend }}) Compare finalization result with files on disk"
assert:
that:
- finalize_1.selected_chain.cert == slurp.results[0].content | b64decode
- finalize_1.selected_chain.chain == slurp.results[1].content | b64decode
- finalize_1.selected_chain.full_chain == slurp.results[2].content | b64decode
- name: Get order information
- name: "({{ select_crypto_backend }}) Get order information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -258,11 +261,11 @@
order_uri: "{{ order.order_uri }}"
register: order_info_3
- name: Show order information
- name: "({{ select_crypto_backend }}) Show order information"
debug:
var: order_info_3
- name: Check order information
- name: "({{ select_crypto_backend }}) Check order information"
assert:
that:
- order_info_3 is not changed
@ -284,7 +287,7 @@
- order_info_3.order_uri == order.order_uri
- order_info_3.account_uri == account.account_uri
- name: Retrieve the cert and intermediate certificate (idempotent)
- name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate (idempotent, but deactivate authzs)"
community.crypto.acme_certificate_order_finalize:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -300,7 +303,7 @@
fullchain_dest: "{{ remote_tmp_dir }}/cert-fullchain.pem"
register: finalize_2
- name: Check finalization result
- name: "({{ select_crypto_backend }}) Check finalization result"
assert:
that:
- finalize_2 is not changed
@ -312,7 +315,7 @@
- finalize_2.selected_chain.full_chain == finalize_2.selected_chain.cert + finalize_2.selected_chain.chain
- finalize_2.selected_chain == finalize_1.selected_chain
- name: Get order information
- name: "({{ select_crypto_backend }}) Get order information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
@ -322,11 +325,11 @@
order_uri: "{{ order.order_uri }}"
register: order_info_4
- name: Show order information
- name: "({{ select_crypto_backend }}) Show order information"
debug:
var: order_info_4
- name: Check order information
- name: "({{ select_crypto_backend }}) Check order information"
assert:
that:
- order_info_4 is not changed
@ -347,3 +350,385 @@
- order_info_4.order.status == 'deactivated'
- order_info_4.order_uri == order.order_uri
- order_info_4.account_uri == account.account_uri
# Test ARI support
- when: acme_supports_ari
block:
- name: "({{ select_crypto_backend }}) Get certificate renewal information"
acme_certificate_renewal_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
# account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert.pem"
register: cert_info
- name: "({{ select_crypto_backend }}) Verify information"
assert:
that:
- cert_info.supports_ari == true
- cert_info.should_renew == false
- cert_info.cert_id is string
- name: "({{ select_crypto_backend }}) Create replacement order 1"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
replaces_cert_id: "{{ cert_info.cert_id }}"
order_creation_error_strategy: fail
register: replacement_order_1
- name: "({{ select_crypto_backend }}) Get replacement order 1 information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_1.order_uri }}"
register: order_info_5
- name: "({{ select_crypto_backend }}) Check replacement order 1"
assert:
that:
- replacement_order_1 is changed
- replacement_order_1.order_uri.startswith('https://' ~ acme_host ~ ':14000/')
- replacement_order_1.challenge_data | length == 1
- replacement_order_1.challenge_data[0].identifier_type == 'dns'
- replacement_order_1.challenge_data[0].identifier == domain_name
- replacement_order_1.challenge_data[0].challenges | length >= 2
- "'http-01' in replacement_order_1.challenge_data[0].challenges"
- "'dns-01' in replacement_order_1.challenge_data[0].challenges"
- replacement_order_1.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/')
- replacement_order_1.challenge_data[0].challenges['http-01'].resource_value is string
- replacement_order_1.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name
- replacement_order_1.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge'
- replacement_order_1.challenge_data[0].challenges['dns-01'].resource_value is string
- replacement_order_1.challenge_data_dns | length == 1
- replacement_order_1.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1
- replacement_order_1.account_uri == account.account_uri
- replacement_order_1.order_uri not in [order.order_uri]
- name: "({{ select_crypto_backend }}) Check replacement order 1 information"
assert:
that:
- order_info_5 is not changed
- order_info_5.authorizations_by_identifier | length == 1
- order_info_5.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns'
- order_info_5.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name
- order_info_5.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending'
- (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending'
- (order_info_5.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending'
- order_info_5.authorizations_by_status['deactivated'] | length == 0
- order_info_5.authorizations_by_status['expired'] | length == 0
- order_info_5.authorizations_by_status['invalid'] | length == 0
- order_info_5.authorizations_by_status['pending'] | length == 1
- order_info_5.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name
- order_info_5.authorizations_by_status['revoked'] | length == 0
- order_info_5.authorizations_by_status['valid'] | length == 0
- order_info_5.order.authorizations | length == 1
- order_info_5.order.authorizations[0] == order_info_5.authorizations_by_identifier['dns:' ~ domain_name].uri
- "'certificate' not in order_info_5.order"
- order_info_5.order.status == 'pending'
- order_info_5.order.replaces == cert_info.cert_id
- order_info_5.order_uri == replacement_order_1.order_uri
- order_info_5.account_uri == account.account_uri
# Right now Pebble does not reject duplicate replacement orders...
- when: false # TODO get Pebble improved
block:
- name: "({{ select_crypto_backend }}) Create replacement order 2 (should fail)"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
replaces_cert_id: "{{ cert_info.cert_id }}"
order_creation_error_strategy: fail
register: replacement_order_2
ignore_errors: true
- name: "({{ select_crypto_backend }}) Check replacement order 2"
assert:
that:
- replacement_order_2 is failed
- >-
replacement_order_2.msg.startswith(
'Failed to start new order for '
~ acme_directory_url
~ '/order-plz with status 409 Conflict. Error urn:ietf:params:acme:error:malformed:'
)
- name: "({{ select_crypto_backend }}) Create replacement order 3 with error handling"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
replaces_cert_id: "{{ cert_info.cert_id }}"
order_creation_error_strategy: retry_without_replaces_cert_id
register: replacement_order_3
- name: "({{ select_crypto_backend }}) Get replacement order 3 information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_3.order_uri }}"
register: order_info_6
- name: "({{ select_crypto_backend }}) Check replacement order 3"
assert:
that:
- replacement_order_3 is changed
- replacement_order_3.order_uri.startswith('https://' ~ acme_host ~ ':14000/')
- replacement_order_3.challenge_data | length == 1
- replacement_order_3.challenge_data[0].identifier_type == 'dns'
- replacement_order_3.challenge_data[0].identifier == domain_name
- replacement_order_3.challenge_data[0].challenges | length >= 2
- "'http-01' in replacement_order_3.challenge_data[0].challenges"
- "'dns-01' in replacement_order_3.challenge_data[0].challenges"
- replacement_order_3.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/')
- replacement_order_3.challenge_data[0].challenges['http-01'].resource_value is string
- replacement_order_3.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name
- replacement_order_3.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge'
- replacement_order_3.challenge_data[0].challenges['dns-01'].resource_value is string
- replacement_order_3.challenge_data_dns | length == 1
- replacement_order_3.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1
- replacement_order_3.account_uri == account.account_uri
- replacement_order_3.order_uri not in [order.order_uri, replacement_order_1.order_uri]
- >-
'Stop passing `replaces` due to error 409 urn:ietf:params:acme:error:malformed when creating ACME order' in replacement_order_3.warnings
- name: "({{ select_crypto_backend }}) Check replacement order 3 information"
assert:
that:
- order_info_6 is not changed
- order_info_6.authorizations_by_identifier | length == 1
- order_info_6.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns'
- order_info_6.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name
- order_info_6.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending'
- (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending'
- (order_info_6.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending'
- order_info_6.authorizations_by_status['deactivated'] | length == 0
- order_info_6.authorizations_by_status['expired'] | length == 0
- order_info_6.authorizations_by_status['invalid'] | length == 0
- order_info_6.authorizations_by_status['pending'] | length == 1
- order_info_6.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name
- order_info_6.authorizations_by_status['revoked'] | length == 0
- order_info_6.authorizations_by_status['valid'] | length == 0
- order_info_6.order.authorizations | length == 1
- order_info_6.order.authorizations[0] == order_info_6.authorizations_by_identifier['dns:' ~ domain_name].uri
- "'certificate' not in order_info_6.order"
- order_info_6.order.status == 'pending'
- order_info_6.order.replaces is not defined
- order_info_6.order_uri == replacement_order_3.order_uri
- order_info_6.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 3"
acme_certificate_deactivate_authz:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_3.order_uri }}"
# Complete replacement order 1
- name: "({{ select_crypto_backend }}) Create HTTP challenges (replacement order 1)"
uri:
url: "http://{{ acme_host }}:5000/http/{{ item.identifier }}/{{ item.challenges['http-01'].resource[('.well-known/acme-challenge/'|length):] }}"
method: PUT
body_format: raw
body: "{{ item.challenges['http-01'].resource_value }}"
headers:
content-type: "application/octet-stream"
loop: "{{ replacement_order_1.challenge_data }}"
when: "'http-01' in item.challenges"
- name: "({{ select_crypto_backend }}) Let the challenge be validated (replacement order 1)"
community.crypto.acme_certificate_order_validate:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_1.order_uri }}"
challenge: http-01
- name: "({{ select_crypto_backend }}) Retrieve the cert and intermediate certificate (replacement order 1)"
community.crypto.acme_certificate_order_finalize:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_1.order_uri }}"
deactivate_authzs: on_success
retrieve_all_alternates: true
csr: "{{ remote_tmp_dir }}/cert.csr"
cert_dest: "{{ remote_tmp_dir }}/cert-repl.pem"
chain_dest: "{{ remote_tmp_dir }}/cert-repl-chain.pem"
fullchain_dest: "{{ remote_tmp_dir }}/cert-repl-fullchain.pem"
# Pebble *does* check against *completed* replacement orders
- when: true
block:
- name: "({{ select_crypto_backend }}) Create replacement order 4 (should fail)"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
replaces_cert_id: "{{ cert_info.cert_id }}"
order_creation_error_strategy: fail
register: replacement_order_4
ignore_errors: true
- name: "({{ select_crypto_backend }}) Check replacement order 4"
assert:
that:
- replacement_order_4 is failed
- replacement_order_4.msg.startswith('Failed to start new order for https://' ~ acme_host)
- >-
' with status 409 Conflict. Error urn:ietf:params:acme:error:malformed: ' in replacement_order_4.msg
- name: "({{ select_crypto_backend }}) Create replacement order 5 with error handling"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
replaces_cert_id: "{{ cert_info.cert_id }}"
order_creation_error_strategy: retry_without_replaces_cert_id
register: replacement_order_5
- name: "({{ select_crypto_backend }}) Get replacement order 5 information"
acme_certificate_order_info:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_5.order_uri }}"
register: order_info_7
- name: "({{ select_crypto_backend }}) Check replacement order 5"
assert:
that:
- replacement_order_5 is changed
- replacement_order_5.order_uri.startswith('https://' ~ acme_host ~ ':14000/')
- replacement_order_5.challenge_data | length == 1
- replacement_order_5.challenge_data[0].identifier_type == 'dns'
- replacement_order_5.challenge_data[0].identifier == domain_name
- replacement_order_5.challenge_data[0].challenges | length >= 2
- "'http-01' in replacement_order_5.challenge_data[0].challenges"
- "'dns-01' in replacement_order_5.challenge_data[0].challenges"
- replacement_order_5.challenge_data[0].challenges['http-01'].resource.startswith('.well-known/acme-challenge/')
- replacement_order_5.challenge_data[0].challenges['http-01'].resource_value is string
- replacement_order_5.challenge_data[0].challenges['dns-01'].record == '_acme-challenge.' ~ domain_name
- replacement_order_5.challenge_data[0].challenges['dns-01'].resource == '_acme-challenge'
- replacement_order_5.challenge_data[0].challenges['dns-01'].resource_value is string
- replacement_order_5.challenge_data_dns | length == 1
- replacement_order_5.challenge_data_dns['_acme-challenge.' ~ domain_name] | length == 1
- replacement_order_5.account_uri == account.account_uri
- replacement_order_5.order_uri not in [order.order_uri, replacement_order_1.order_uri]
- >-
'Stop passing `replaces` due to error 409 urn:ietf:params:acme:error:malformed when creating ACME order' in replacement_order_5.warnings
- name: "({{ select_crypto_backend }}) Check replacement order 5 information"
assert:
that:
- order_info_7 is not changed
- order_info_7.authorizations_by_identifier | length == 1
- order_info_7.authorizations_by_identifier['dns:' ~ domain_name].identifier.type == 'dns'
- order_info_7.authorizations_by_identifier['dns:' ~ domain_name].identifier.value == domain_name
- order_info_7.authorizations_by_identifier['dns:' ~ domain_name].status == 'pending'
- (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'http-01') | first).status == 'pending'
- (order_info_7.authorizations_by_identifier['dns:' ~ domain_name].challenges | selectattr('type', 'equalto', 'dns-01') | first).status == 'pending'
- order_info_7.authorizations_by_status['deactivated'] | length == 0
- order_info_7.authorizations_by_status['expired'] | length == 0
- order_info_7.authorizations_by_status['invalid'] | length == 0
- order_info_7.authorizations_by_status['pending'] | length == 1
- order_info_7.authorizations_by_status['pending'][0] == 'dns:' ~ domain_name
- order_info_7.authorizations_by_status['revoked'] | length == 0
- order_info_7.authorizations_by_status['valid'] | length == 0
- order_info_7.order.authorizations | length == 1
- order_info_7.order.authorizations[0] == order_info_7.authorizations_by_identifier['dns:' ~ domain_name].uri
- "'certificate' not in order_info_7.order"
- order_info_7.order.status == 'pending'
- order_info_7.order.replaces is not defined
- order_info_7.order_uri == replacement_order_5.order_uri
- order_info_7.account_uri == account.account_uri
- name: "({{ select_crypto_backend }}) Deactivate authzs for replacement order 5"
acme_certificate_deactivate_authz:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
order_uri: "{{ replacement_order_5.order_uri }}"
# Test invalid profile
- when: acme_supports_profiles
block:
- name: "({{ select_crypto_backend }}) Create order with invalid profile (should fail)"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
profile: does-not-exist
order_creation_error_strategy: fail
register: invalid_profile_order
ignore_errors: true
- name: "({{ select_crypto_backend }}) Check invalid profile order"
assert:
that:
- invalid_profile_order is failed
- invalid_profile_order.msg == "The ACME CA does not support selected profile 'does-not-exist'."
# Test profile when server does not support it
- when: not acme_supports_profiles
block:
- name: "({{ select_crypto_backend }}) Create order with profile when server does not support it (should fail)"
acme_certificate_order_create:
acme_directory: "{{ acme_directory_url }}"
acme_version: 2
validate_certs: false
account_key_src: "{{ remote_tmp_dir }}/accountkey.pem"
select_crypto_backend: "{{ select_crypto_backend }}"
csr: "{{ remote_tmp_dir }}/cert.csr"
profile: default
register: profile_without_server_support
ignore_errors: true
- name: "({{ select_crypto_backend }}) Check profile without server support order"
assert:
that:
- profile_without_server_support is failed
- profile_without_server_support.msg == 'The ACME CA does not support profiles. Please omit the "profile" option.'

View File

@ -6,6 +6,8 @@
.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate
docs/docsite/rst/guide_selfsigned.rst rstcheck
plugins/modules/acme_account_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error

View File

@ -5,6 +5,8 @@
.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate
.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate
plugins/modules/acme_account_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error

View File

@ -1,5 +1,7 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/modules/acme_account_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_info.py validate-modules:return-syntax-error
plugins/modules/acme_certificate_order_validate.py validate-modules:return-syntax-error

View File

@ -1,5 +1,7 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/ecs_certificate.py validate-modules:invalid-documentation
plugins/modules/get_certificate.py validate-modules:invalid-documentation
plugins/modules/luks_device.py validate-modules:invalid-documentation

View File

@ -1,5 +1,7 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/lookup/gpg_fingerprint.py validate-modules:invalid-documentation
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/ecs_certificate.py validate-modules:invalid-documentation
plugins/modules/get_certificate.py validate-modules:invalid-documentation
plugins/modules/luks_device.py validate-modules:invalid-documentation

View File

@ -1,4 +1,6 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/luks_device.py validate-modules:invalid-documentation
tests/ee/roles/smoke/library/smoke_ipaddress.py shebang
tests/ee/roles/smoke/library/smoke_pyyaml.py shebang

View File

@ -1,3 +1,5 @@
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/luks_device.py validate-modules:invalid-documentation
tests/ee/roles/smoke/library/smoke_ipaddress.py shebang
tests/ee/roles/smoke/library/smoke_pyyaml.py shebang

View File

@ -5,6 +5,8 @@
.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate
.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate
docs/docsite/rst/guide_selfsigned.rst rstcheck
plugins/modules/acme_certificate.py validate-modules:invalid-documentation
plugins/modules/acme_certificate_order_create.py validate-modules:invalid-documentation
plugins/modules/acme_challenge_cert_helper.py validate-modules:return-syntax-error
plugins/modules/ecs_certificate.py validate-modules:invalid-documentation
plugins/modules/get_certificate.py validate-modules:invalid-documentation