Add new ACME modules for working with orders. (#757)

This commit is contained in:
Felix Fontein
2025-01-12 17:10:58 +01:00
committed by GitHub
parent 072318466e
commit 49354f2121
14 changed files with 2280 additions and 2 deletions

View File

@@ -0,0 +1,413 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_order_create
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
short_description: Create an ACME v2 order
description:
- Creates an ACME v2 order. This is the first step of obtaining a new certificate
with the L(ACME protocol,https://tools.ietf.org/html/rfc8555) from a Certificate
Authority such as L(Let's Encrypt,https://letsencrypt.org/) or
L(Buypass,https://www.buypass.com/). This module does not support ACME v1, the
original version of the ACME protocol before standardization.
- The current implementation supports the V(http-01), V(dns-01) and V(tls-alpn-01)
challenges.
- This module needs to be used in conjunction with the
M(community.crypto.acme_certificate_order_validate) and.
M(community.crypto.acme_certificate_order_finalize) module.
An order can be effectively deactivated with the
M(community.crypto.acme_certificate_deactivate_authz) module.
Note that both modules require the output RV(order_uri) of this module.
- To create or modify ACME accounts, use the M(community.crypto.acme_account) module.
This module will I(not) create or update ACME accounts.
- Between the call of this module and M(community.crypto.acme_certificate_order_finalize),
you have to fulfill the required steps for the chosen challenge by whatever means necessary.
For V(http-01) that means creating the necessary challenge file on the destination webserver.
For V(dns-01) the necessary dns record has to be created. For V(tls-alpn-01) the necessary
certificate has to be created and served. It is I(not) the responsibility of this module to
perform these steps.
- For details on how to fulfill these challenges, you might have to read through
L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8)
and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3).
Also, consider the examples provided for this module.
- The module includes support for IP identifiers according to
the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html) ACME extension.
seealso:
- module: community.crypto.acme_certificate_order_validate
description: Validate pending authorizations of an ACME order.
- module: community.crypto.acme_certificate_order_finalize
description: Finalize an ACME order after satisfying the challenges.
- module: community.crypto.acme_certificate_order_info
description: Obtain information for an ACME order.
- module: community.crypto.acme_certificate_deactivate_authz
description: Deactivate all authorizations (authz) of an ACME order, effectively deactivating
the order itself.
- module: community.crypto.acme_certificate_renewal_info
description: Determine whether a certificate should be renewed.
- name: The Let's Encrypt documentation
description: Documentation for the Let's Encrypt Certification Authority.
Provides useful information for example on rate limits.
link: https://letsencrypt.org/docs/
- name: Buypass Go SSL
description: Documentation for the Buypass Certification Authority.
Provides useful information for example on rate limits.
link: https://www.buypass.com/ssl/products/acme
- name: Automatic Certificate Management Environment (ACME)
description: The specification of the ACME protocol (RFC 8555).
link: https://tools.ietf.org/html/rfc8555
- name: ACME TLS ALPN Challenge Extension
description: The specification of the V(tls-alpn-01) challenge (RFC 8737).
link: https://www.rfc-editor.org/rfc/rfc8737.html
- module: community.crypto.acme_challenge_cert_helper
description: Helps preparing V(tls-alpn-01) challenges.
- module: community.crypto.openssl_privatekey
description: Can be used to create private keys (both for certificates and accounts).
- module: community.crypto.openssl_privatekey_pipe
description: Can be used to create private keys without writing it to disk (both for certificates and accounts).
- module: community.crypto.openssl_csr
description: Can be used to create a Certificate Signing Request (CSR).
- module: community.crypto.openssl_csr_pipe
description: Can be used to create a Certificate Signing Request (CSR) without writing it to disk.
- module: community.crypto.acme_account
description: Allows to create, modify or delete an ACME account.
- module: community.crypto.acme_inspect
description: Allows to debug problems.
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.acme.certificate
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
attributes:
check_mode:
support: none
diff_mode:
support: none
idempotent:
support: none
options:
deactivate_authzs:
description:
- "Deactivate authentication objects (authz) when issuing the certificate
failed."
- "Authentication objects are bound to an account key and remain valid
for a certain amount of time, and can be used to issue certificates
without having to re-authenticate the domain. This can be a security
concern."
type: bool
default: true
replaces_cert_id:
description:
- If provided, will request the order to replace the certificate identified by this certificate ID
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
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
M(community.crypto.acme_certificate_deactivate_authz) module.
type: str
profile:
description:
- Chose a specific profile for certificate selection. The available profiles depend on the CA.
- See L(a blog post by Let's Encrypt, https://letsencrypt.org/2025/01/09/acme-profiles/) and
L(draft-aaron-acme-profiles-00, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
for more information.
type: str
'''
EXAMPLES = r'''
### Example with HTTP-01 challenge ###
- name: Create a challenge for sample.com using a account key from a variable
community.crypto.acme_certificate_order_create:
account_key_content: "{{ account_private_key }}"
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key from Hashi Vault
community.crypto.acme_certificate_order_create:
account_key_content: >-
{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/account_private_key:value') }}
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key file
community.crypto.acme_certificate_order_create:
account_key_src: /etc/pki/cert/private/account.key
csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Copy http-01 challenges
# ansible.builtin.copy:
# dest: /var/www/{{ item.identifier }}/{{ item.challenges['http-01'].resource }}
# content: "{{ item.challenges['http-01'].resource_value }}"
# loop: "{{ sample_com_challenge.challenge_data }}"
# when: "'http-01' in item.challenges"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: http-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
### Example with DNS challenge against production ACME server ###
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Create DNS records for dns-01 challenges
# community.aws.route53:
# zone: sample.com
# record: "{{ item.key }}"
# type: TXT
# ttl: 60
# state: present
# wait: true
# # Note: item.value is a list of TXT entries, and route53
# # requires every entry to be enclosed in quotes
# value: "{{ item.value | map('community.dns.quote_txt', always_quote=true) | list }}"
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
RETURN = '''
challenge_data:
description:
- For every identifier, provides the challenge information.
- Only challenges which are not yet valid are returned.
returned: changed
type: list
elements: dict
contains:
identifier:
description:
- The identifier for this challenge.
type: str
sample: example.com
identifier_type:
description:
- The identifier's type.
- V(dns) for DNS names, and V(ip) for IP addresses.
type: str
choices:
- dns
- ip
sample: dns
challenges:
description:
- Information for different challenge types supported for this identifier.
type: dict
contains:
http-01:
description:
- Information for V(http-01) authorization.
- The server needs to make the path RV(challenge_data[].challenges.http-01.resource)
accessible via HTTP (which might redirect to HTTPS). A C(GET) operation to this path
needs to provide the value from RV(challenge_data[].challenges.http-01.resource_value).
returned: if the identifier supports V(http-01) authorization
type: dict
contains:
resource:
description:
- The path the value has to be provided under.
returned: success
type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_value:
description:
- The value the resource has to produce for the validation.
returned: success
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
dns-01:
description:
- Information for V(dns-01) authorization.
- A DNS TXT record needs to be created with the record name RV(challenge_data[].challenges.dns-01.record)
and value RV(challenge_data[].challenges.dns-01.resource_value).
returned: if the identifier supports V(dns-01) authorization
type: dict
contains:
resource:
description:
- Always contains the string V(_acme-challenge).
type: str
sample: _acme-challenge
resource_value:
description:
- The value the resource has to produce for the validation.
returned: success
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record:
description: The full DNS record's name for the challenge.
returned: success
type: str
sample: _acme-challenge.example.com
tls-alpn-01:
description:
- Information for V(tls-alpn-01) authorization.
- A certificate needs to be created for the DNS name RV(challenge_data[].challenges.tls-alpn-01.resource)
with acmeValidation X.509 extension of value RV(challenge_data[].challenges.tls-alpn-01.resource_value).
This certificate needs to be served when the application-layer protocol C(acme-tls/1) is negotiated for
a HTTPS connection to port 443 with the SNI extension for the domain name
(RV(challenge_data[].challenges.tls-alpn-01.resource_original)) being validated.
- See U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3) for details.
returned: if the identifier supports V(tls-alpn-01) authorization
type: dict
contains:
resource:
description:
- The DNS name for DNS identifiers, and the reverse DNS mapping (RFC1034, RFC3596) for IP addresses.
returned: success
type: str
sample: example.com
resource_original:
description:
- The original identifier including type identifier.
returned: success
type: str
sample: dns:example.com
resource_value:
description:
- The value the resource has to produce for the validation.
- "B(Note:) this return value contains a Base64 encoded version of the correct
binary blob which has to be put into the acmeValidation X.509 extension; see
U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3) for details. To do this,
you might need the P(ansible.builtin.b64decode#filter) Jinja filter to extract
the binary blob from this return value."
returned: success
type: str
sample: AAb=
challenge_data_dns:
description:
- List of TXT values per DNS record for V(dns-01) challenges.
- Only challenges which are not yet valid are returned.
returned: success
type: dict
order_uri:
description: ACME order URI.
returned: success
type: str
account_uri:
description: ACME account URI.
returned: success
type: str
'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.certificate import (
ACMECertificateClient,
)
def main():
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.update_argspec(
deactivate_authzs=dict(type='bool', default=True),
replaces_cert_id=dict(type='str'),
profile=dict(type='str'),
)
module = argument_spec.create_ansible_module()
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
profile = module.params['profile']
if profile is not None:
meta_profiles = (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:
raise ModuleFailException(msg='The ACME CA does not support selected profile {0!r}.'.format(profile))
order = None
done = False
try:
order = client.create_order(replaces_cert_id=module.params['replaces_cert_id'], profile=profile)
client.check_that_authorizations_can_be_used(order)
done = True
finally:
if module.params['deactivate_authzs'] and order and not done:
client.deactivate_authzs(order)
data, data_dns = client.get_challenges_data(order)
module.exit_json(
changed=True,
order_uri=order.url,
account_uri=client.client.account_uri,
challenge_data=data,
challenge_data_dns=data_dns,
)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,439 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_order_finalize
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
short_description: Finalize an ACME v2 order
description:
- Finalizes an ACME v2 order and obtains the certificate and certificate chains.
This is the final step of obtaining a new certificate with the
L(ACME protocol,https://tools.ietf.org/html/rfc8555) from a Certificate
Authority such as L(Let's Encrypt,https://letsencrypt.org/) or
L(Buypass,https://www.buypass.com/). This module does not support ACME v1, the
original version of the ACME protocol before standardization.
- This module needs to be used in conjunction with the
M(community.crypto.acme_certificate_order_create) and.
M(community.crypto.acme_certificate_order_validate) modules.
seealso:
- module: community.crypto.acme_certificate_order_create
description: Create an ACME order.
- module: community.crypto.acme_certificate_order_validate
description: Validate pending authorizations of an ACME order.
- module: community.crypto.acme_certificate_order_info
description: Obtain information for an ACME order.
- name: The Let's Encrypt documentation
description: Documentation for the Let's Encrypt Certification Authority.
Provides useful information for example on rate limits.
link: https://letsencrypt.org/docs/
- name: Buypass Go SSL
description: Documentation for the Buypass Certification Authority.
Provides useful information for example on rate limits.
link: https://www.buypass.com/ssl/products/acme
- name: Automatic Certificate Management Environment (ACME)
description: The specification of the ACME protocol (RFC 8555).
link: https://tools.ietf.org/html/rfc8555
- module: community.crypto.certificate_complete_chain
description: Allows to find the root certificate for the returned fullchain.
- module: community.crypto.acme_certificate_revoke
description: Allows to revoke certificates.
- module: community.crypto.acme_inspect
description: Allows to debug problems.
- module: community.crypto.acme_certificate_deactivate_authz
description: Allows to deactivate (invalidate) ACME v2 orders.
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.acme.certificate
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
- community.crypto.attributes.files
attributes:
check_mode:
support: none
diff_mode:
support: none
safe_file_operations:
support: full
idempotent:
support: full
options:
order_uri:
description:
- The order URI provided by RV(community.crypto.acme_certificate_order_create#module:order_uri).
type: str
required: true
cert_dest:
description:
- "The destination file for the certificate."
type: path
fullchain_dest:
description:
- "The destination file for the full chain (that is, a certificate followed
by chain of intermediate certificates)."
type: path
chain_dest:
description:
- If specified, the intermediate certificate will be written to this file.
type: path
deactivate_authzs:
description:
- "Deactivate authentication objects (authz) after issuing a certificate,
or when issuing the certificate failed."
- V(never) never deactivates them.
- V(always) always deactivates them in cases of errors or when the certificate was issued.
- V(on_error) only deactivates them in case of errors.
- V(on_success) only deactivates them in case the certificate was successfully issued.
- "Authentication objects are bound to an account key and remain valid
for a certain amount of time, and can be used to issue certificates
without having to re-authenticate the domain. This can be a security
concern."
type: str
choices:
- never
- on_error
- on_success
- always
default: always
retrieve_all_alternates:
description:
- "When set to V(true), will retrieve all alternate trust chains offered by the ACME CA.
These will not be written to disk, but will be returned together with the main
chain as RV(all_chains). See the documentation for the RV(all_chains) return
value for details."
type: bool
default: false
select_chain:
description:
- "Allows to specify criteria by which an (alternate) trust chain can be selected."
- "The list of criteria will be processed one by one until a chain is found
matching a criterium. If such a chain is found, it will be used by the
module instead of the default chain."
- "If a criterium matches multiple chains, the first one matching will be
returned. The order is determined by the ordering of the C(Link) headers
returned by the ACME server and might not be deterministic."
- "Every criterium can consist of multiple different conditions, like O(select_chain[].issuer)
and O(select_chain[].subject). For the criterium to match a chain, all conditions must apply
to the same certificate in the chain."
- "This option can only be used with the C(cryptography) backend."
type: list
elements: dict
suboptions:
test_certificates:
description:
- "Determines which certificates in the chain will be tested."
- "V(all) tests all certificates in the chain (excluding the leaf, which is
identical in all chains)."
- "V(first) only tests the first certificate in the chain, that is the one which
signed the leaf."
- "V(last) only tests the last certificate in the chain, that is the one furthest
away from the leaf. Its issuer is the root certificate of this chain."
type: str
default: all
choices: [first, last, all]
issuer:
description:
- "Allows to specify parts of the issuer of a certificate in the chain must
have to be selected."
- "If O(select_chain[].issuer) is empty, any certificate will match."
- 'An example value would be V({"commonName": "My Preferred CA Root"}).'
type: dict
subject:
description:
- "Allows to specify parts of the subject of a certificate in the chain must
have to be selected."
- "If O(select_chain[].subject) is empty, any certificate will match."
- 'An example value would be V({"CN": "My Preferred CA Intermediate"})'
type: dict
subject_key_identifier:
description:
- "Checks for the SubjectKeyIdentifier extension. This is an identifier based
on the private key of the intermediate certificate."
- "The identifier must be of the form
V(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)."
type: str
authority_key_identifier:
description:
- "Checks for the AuthorityKeyIdentifier extension. This is an identifier based
on the private key of the issuer of the intermediate certificate."
- "The identifier must be of the form
V(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
type: str
'''
EXAMPLES = r'''
### Example with HTTP-01 challenge ###
- name: Create a challenge for sample.com using a account key from a variable
community.crypto.acme_certificate_order_create:
account_key_content: "{{ account_private_key }}"
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key from Hashi Vault
community.crypto.acme_certificate_order_create:
account_key_content: >-
{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/account_private_key:value') }}
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key file
community.crypto.acme_certificate_order_create:
account_key_src: /etc/pki/cert/private/account.key
csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Copy http-01 challenges
# ansible.builtin.copy:
# dest: /var/www/{{ item.identifier }}/{{ item.challenges['http-01'].resource }}
# content: "{{ item.challenges['http-01'].resource_value }}"
# loop: "{{ sample_com_challenge.challenge_data }}"
# when: "'http-01' in item.challenges"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: http-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
### Example with DNS challenge against production ACME server ###
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Create DNS records for dns-01 challenges
# community.aws.route53:
# zone: sample.com
# record: "{{ item.key }}"
# type: TXT
# ttl: 60
# state: present
# wait: true
# # Note: item.value is a list of TXT entries, and route53
# # requires every entry to be enclosed in quotes
# value: "{{ item.value | map('community.dns.quote_txt', always_quote=true) | list }}"
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
RETURN = '''
account_uri:
description: ACME account URI.
returned: success
type: str
all_chains:
description:
- When O(retrieve_all_alternates=true), the module will query the ACME server for
alternate chains. This return value will contain a list of all chains returned,
the first entry being the main chain returned by the server.
- See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2)
for details.
returned: success and O(retrieve_all_alternates=true)
type: list
elements: dict
contains:
cert:
description:
- The leaf certificate itself, in PEM format.
type: str
returned: always
chain:
description:
- The certificate chain, excluding the root, as concatenated PEM certificates.
type: str
returned: always
full_chain:
description:
- The certificate chain, excluding the root, but including the leaf certificate,
as concatenated PEM certificates.
type: str
returned: always
selected_chain:
description:
- The selected certificate chain.
- If O(select_chain) is not specified, this will be the main chain returned by the
ACME server.
returned: success
type: dict
contains:
cert:
description:
- The leaf certificate itself, in PEM format.
type: str
returned: always
chain:
description:
- The certificate chain, excluding the root, as concatenated PEM certificates.
type: str
returned: always
full_chain:
description:
- The certificate chain, excluding the root, but including the leaf certificate,
as concatenated PEM certificates.
type: str
returned: always
'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.certificate import (
ACMECertificateClient,
)
def main():
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
cert_dest=dict(type='path'),
fullchain_dest=dict(type='path'),
chain_dest=dict(type='path'),
deactivate_authzs=dict(type='str', default='always', choices=['never', 'always', 'on_error', 'on_success']),
retrieve_all_alternates=dict(type='bool', default=False),
select_chain=dict(type='list', elements='dict', options=dict(
test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']),
issuer=dict(type='dict'),
subject=dict(type='dict'),
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
)
module = argument_spec.create_ansible_module()
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
select_chain_matcher = client.parse_select_chain(module.params['select_chain'])
other = dict()
done = False
order = None
try:
# Step 1: load order
order = client.load_order()
download_all_chains = len(select_chain_matcher) > 0 or module.params['retrieve_all_alternates']
changed = False
if order.status == 'valid':
# Step 2 and 3: download certificate(s) and chain(s)
cert, alternate_chains = client.download_certificate(
order,
download_all_chains=download_all_chains,
)
else:
client.check_that_authorizations_can_be_used(order)
# Step 2: wait for authorizations to validate
pending_authzs = client.collect_pending_authzs(order)
client.wait_for_validation(pending_authzs)
# Step 3: finalize order, wait, then download certificate(s) and chain(s)
cert, alternate_chains = client.get_certificate(
order,
download_all_chains=download_all_chains,
)
changed = True
# Step 4: pick chain, write certificates, and provide return values
if alternate_chains is not None:
# Prepare return value for all alternate chains
if module.params['retrieve_all_alternates']:
all_chains = [cert.to_json()]
for alt_chain in alternate_chains:
all_chains.append(alt_chain.to_json())
other['all_chains'] = all_chains
# Try to select alternate chain depending on criteria
if select_chain_matcher:
matching_chain = client.find_matching_chain([cert] + alternate_chains, select_chain_matcher)
if matching_chain:
cert = matching_chain
else:
module.debug('Found no matching alternative chain')
if client.write_cert_chain(
cert,
cert_dest=module.params['cert_dest'],
fullchain_dest=module.params['fullchain_dest'],
chain_dest=module.params['chain_dest'],
):
changed = True
done = True
finally:
if (
module.params['deactivate_authzs'] == 'always' or
(module.params['deactivate_authzs'] == 'on_success' and done) or
(module.params['deactivate_authzs'] == 'on_error' and not done)
):
if order:
client.deactivate_authzs(order)
module.exit_json(
changed=changed,
account_uri=client.client.account_uri,
selected_chain=cert.to_json(),
**other
)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,402 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_order_info
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
short_description: Obtain information for an ACME v2 order
description:
- Obtain information for an ACME v2 order.
This can be used during the process of obtaining a new certificate with the
L(ACME protocol,https://tools.ietf.org/html/rfc8555) from a Certificate
Authority such as L(Let's Encrypt,https://letsencrypt.org/) or
L(Buypass,https://www.buypass.com/). This module does not support ACME v1, the
original version of the ACME protocol before standardization.
- This module needs to be used in conjunction with the
M(community.crypto.acme_certificate_order_create),
M(community.crypto.acme_certificate_order_validate), and
M(community.crypto.acme_certificate_order_finalize) modules.
seealso:
- module: community.crypto.acme_certificate_order_create
description: Create an ACME order.
- module: community.crypto.acme_certificate_order_validate
description: Validate pending authorizations of an ACME order.
- module: community.crypto.acme_certificate_order_finalize
description: Finalize an ACME order after satisfying the challenges.
- name: Automatic Certificate Management Environment (ACME)
description: The specification of the ACME protocol (RFC 8555).
link: https://tools.ietf.org/html/rfc8555
- name: ACME TLS ALPN Challenge Extension
description: The specification of the V(tls-alpn-01) challenge (RFC 8737).
link: https://www.rfc-editor.org/rfc/rfc8737.html
- module: community.crypto.acme_inspect
description: Allows to debug problems.
- module: community.crypto.acme_certificate_deactivate_authz
description: Allows to deactivate (invalidate) ACME v2 orders.
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
- community.crypto.attributes.idempotent_not_modify_state
- community.crypto.attributes.info_module
options:
order_uri:
description:
- The order URI provided by RV(community.crypto.acme_certificate_order_create#module:order_uri).
type: str
required: true
'''
EXAMPLES = r'''
- name: Create a challenge for sample.com using a account key from a variable
community.crypto.acme_certificate_order_create:
account_key_content: "{{ account_private_key }}"
csr: /etc/pki/cert/csr/sample.com.csr
register: order
- name: Obtain information on the order
community.crypto.acme_certificate_order_info:
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ order.order_uri }}"
register: order_info
- name: Show information
ansible.builtin.debug:
var: order_info
'''
RETURN = '''
account_uri:
description: ACME account URI.
returned: success
type: str
order_uri:
description: ACME order URI.
returned: success
type: str
order:
description:
- The order object.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.3) for its specification.
returned: success
type: dict
contains:
status:
description:
- The status of this order.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes.
type: str
returned: always
choices:
- pending
- ready
- processing
- valid
- invalid
expires:
description:
- The timestamp after which the server will consider this order invalid.
- Encoded in the format specified in L(RFC 3339, https://www.rfc-editor.org/rfc/rfc3339).
type: str
returned: if RV(order.status) is V(pending) or V(valid), and sometimes in other situations
identifiers:
description:
- An array of identifier objects that the order pertains to.
returned: always
type: list
elements: dict
contains:
type:
description:
- The type of identifier.
- So far V(dns) and V(ip) are defined values.
type: str
returned: always
sample: dns
choices:
- dns
- ip
value:
description:
- The identifier itself.
type: str
returned: always
sample: example.com
notBefore:
description:
- The requested value of the C(notBefore) field in the certificate.
- Encoded in the date format defined in L(RFC 3339, https://www.rfc-editor.org/rfc/rfc3339).
type: str
returned: depending on order
notAfter (optional, string):
description:
- The requested value of the C(notAfter) field in the certificate.
- Encoded in the date format defined in L(RFC 3339, https://www.rfc-editor.org/rfc/rfc3339).
type: str
returned: depending on order
error:
description:
- The error that occurred while processing the order, if any.
- This field is structured as a L(problem document according to RFC 7807, https://www.rfc-editor.org/rfc/rfc7807).
type: dict
returned: sometimes
authorizations:
description:
- For pending orders, the authorizations that the client needs to complete before the
requested certificate can be issued, including unexpired authorizations that the client
has completed in the past for identifiers specified in the order.
- The authorizations required are dictated by server policy; there may not be a 1:1
relationship between the order identifiers and the authorizations required.
- For final orders (in the V(valid) or V(invalid) state), the authorizations that were
completed. Each entry is a URL from which an authorization can be fetched with a POST-as-GET request.
- The authorizations themselves are returned as RV(authorizations_by_identifier).
type: list
elements: str
returned: always
finalize:
description:
- A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the
order. The result of a successful finalization will be the population of the certificate URL for the order.
type: str
returned: always
certificate:
description:
- A URL for the certificate that has been issued in response to this order.
type: str
returned: when the certificate has been issued
authorizations_by_identifier:
description:
- A dictionary mapping identifiers to their authorization objects.
returned: success
type: dict
contains:
identifier:
description:
- The keys in this dictionary are the identifiers. C(identifier) is a placeholder used in the documentation.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.4) for how authorization objects look like.
type: dict
contains:
identifier:
description:
- The identifier that the account is authorized to represent.
type: dict
returned: always
contains:
type:
description:
- The type of identifier.
- So far V(dns) and V(ip) are defined values.
type: str
returned: always
sample: dns
choices:
- dns
- ip
value:
description:
- The identifier itself.
type: str
returned: always
sample: example.com
status:
description:
- The status of this authorization.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes.
type: str
choices:
- pending
- valid
- invalid
- deactivated
- expired
- revoked
returned: always
expires:
description:
- The timestamp after which the server will consider this authorization invalid.
- Encoded in the format specified in L(RFC 3339, https://www.rfc-editor.org/rfc/rfc3339).
type: str
returned: if RV(authorizations_by_identifier.identifier.status=valid), and sometimes in other situations
challenges:
description:
- For pending authorizations, the challenges that the client can fulfill in order to prove
possession of the identifier.
- For valid authorizations, the challenge that was validated.
- For invalid authorizations, the challenge that was attempted and failed.
- Each array entry is an object with parameters required to validate the challenge.
A client should attempt to fulfill one of these challenges, and a server should consider
any one of the challenges sufficient to make the authorization valid.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-8) for the general structure. The structure
of every entry depends on the challenge's type. For C(tls-alpn-01) challenges, the structure is
defined in U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3).
type: list
elements: dict
returned: always
contains:
type:
description:
- The type of challenge encoded in the object.
type: str
returned: always
choices:
- http-01
- dns-01
- tls-alpn-01
url:
description:
- The URL to which a response can be posted.
type: str
returned: always
status:
description:
- The status of this challenge.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes.
type: str
choices:
- pending
- processing
- valid
- invalid
returned: always
validated:
description:
- The time at which the server validated this challenge.
- Encoded in the format specified in L(RFC 3339, https://www.rfc-editor.org/rfc/rfc3339).
type: str
returned: always if RV(authorizations_by_identifier.identifier.challenges[].type=valid), otherwise in some situations
error:
description:
- Error that occurred while the server was validating the challenge, if any.
- This field is structured as a L(problem document according to RFC 7807, https://www.rfc-editor.org/rfc/rfc7807).
type: dict
returned: always if RV(authorizations_by_identifier.identifier.challenges[].type=invalid), otherwise in some situations
wildcard:
description:
- This field B(must) be present and true for authorizations created as a result of a
C(newOrder) request containing a DNS identifier with a value that was a wildcard
domain name. For other authorizations, it B(must) be absent.
- Wildcard domain names are described in U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.3)
of the ACME specification.
type: bool
returned: sometimes
authorizations_by_status:
description:
- For every status, a list of identifiers whose authorizations have this status.
returned: success
type: dict
contains:
pending:
description:
- A list of all identifiers whose authorizations are in the C(pending) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
invalid:
description:
- A list of all identifiers whose authorizations are in the C(invalid) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
valid:
description:
- A list of all identifiers whose authorizations are in the C(valid) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
revoked:
description:
- A list of all identifiers whose authorizations are in the C(revoked) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
deactivated:
description:
- A list of all identifiers whose authorizations are in the C(deactivated) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
expired:
description:
- A list of all identifiers whose authorizations are in the C(expired) state.
- See U(https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6) for state changes
of authorizations.
type: list
elements: str
returned: always
'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.certificate import (
ACMECertificateClient,
)
def main():
argument_spec = create_default_argspec(with_certificate=False)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
order = client.load_order()
authorizations_by_identifier = dict()
authorizations_by_status = {
'pending': [],
'invalid': [],
'valid': [],
'revoked': [],
'deactivated': [],
'expired': [],
}
for identifier, authz in order.authorizations.items():
authorizations_by_identifier[identifier] = authz.to_json()
authorizations_by_status[authz.status].append(identifier)
module.exit_json(
changed=False,
account_uri=client.client.account_uri,
order_uri=order.url,
order=order.data,
authorizations_by_identifier=authorizations_by_identifier,
authorizations_by_status=authorizations_by_status,
)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,339 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 Felix Fontein <felix@fontein.de>
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: acme_certificate_order_validate
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
short_description: Validate authorizations of an ACME v2 order
description:
- Validates pending authorizations of an ACME v2 order.
This is the second to last step of obtaining a new certificate with the
L(ACME protocol,https://tools.ietf.org/html/rfc8555) from a Certificate
Authority such as L(Let's Encrypt,https://letsencrypt.org/) or
L(Buypass,https://www.buypass.com/). This module does not support ACME v1, the
original version of the ACME protocol before standardization.
- This module needs to be used in conjunction with the
M(community.crypto.acme_certificate_order_create) and
M(community.crypto.acme_certificate_order_finalize) modules.
seealso:
- module: community.crypto.acme_certificate_order_create
description: Create an ACME order.
- module: community.crypto.acme_certificate_order_finalize
description: Finalize an ACME order after satisfying the challenges.
- module: community.crypto.acme_certificate_order_info
description: Obtain information for an ACME order.
- name: The Let's Encrypt documentation
description: Documentation for the Let's Encrypt Certification Authority.
Provides useful information for example on rate limits.
link: https://letsencrypt.org/docs/
- name: Buypass Go SSL
description: Documentation for the Buypass Certification Authority.
Provides useful information for example on rate limits.
link: https://www.buypass.com/ssl/products/acme
- name: Automatic Certificate Management Environment (ACME)
description: The specification of the ACME protocol (RFC 8555).
link: https://tools.ietf.org/html/rfc8555
- name: ACME TLS ALPN Challenge Extension
description: The specification of the V(tls-alpn-01) challenge (RFC 8737).
link: https://www.rfc-editor.org/rfc/rfc8737.html
- module: community.crypto.acme_challenge_cert_helper
description: Helps preparing V(tls-alpn-01) challenges.
- module: community.crypto.acme_inspect
description: Allows to debug problems.
- module: community.crypto.acme_certificate_deactivate_authz
description: Allows to deactivate (invalidate) ACME v2 orders.
extends_documentation_fragment:
- community.crypto.acme.basic
- community.crypto.acme.account
- community.crypto.attributes
- community.crypto.attributes.actiongroup_acme
- community.crypto.attributes.files
attributes:
check_mode:
support: none
diff_mode:
support: none
safe_file_operations:
support: full
idempotent:
support: full
options:
challenge:
description:
- The challenge to be performed for every pending authorization.
- Must be provided if there is at least one pending authorization.
- In case of authorization reuse, or in case of CAs which use External Account Binding
and other means of validating certificate assurance, it might not be necessary
to provide this option.
type: str
choices:
- 'http-01'
- 'dns-01'
- 'tls-alpn-01'
order_uri:
description:
- The order URI provided by RV(community.crypto.acme_certificate_order_create#module:order_uri).
type: str
required: true
deactivate_authzs:
description:
- "Deactivate authentication objects (authz) in case an error happens."
- "Authentication objects are bound to an account key and remain valid
for a certain amount of time, and can be used to issue certificates
without having to re-authenticate the domain. This can be a security
concern."
type: bool
default: true
'''
EXAMPLES = r'''
### Example with HTTP-01 challenge ###
- name: Create a challenge for sample.com using a account key from a variable
community.crypto.acme_certificate_order_create:
account_key_content: "{{ account_private_key }}"
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key from Hashi Vault
community.crypto.acme_certificate_order_create:
account_key_content: >-
{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/account_private_key:value') }}
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Alternative first step:
- name: Create a challenge for sample.com using a account key file
community.crypto.acme_certificate_order_create:
account_key_src: /etc/pki/cert/private/account.key
csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Copy http-01 challenges
# ansible.builtin.copy:
# dest: /var/www/{{ item.identifier }}/{{ item.challenges['http-01'].resource }}
# content: "{{ item.challenges['http-01'].resource_value }}"
# loop: "{{ sample_com_challenge.challenge_data }}"
# when: "'http-01' in item.challenges"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: http-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
### Example with DNS challenge against production ACME server ###
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
# Perform the necessary steps to fulfill the challenge. For example:
#
# - name: Create DNS records for dns-01 challenges
# community.aws.route53:
# zone: sample.com
# record: "{{ item.key }}"
# type: TXT
# ttl: 60
# state: present
# wait: true
# # Note: item.value is a list of TXT entries, and route53
# # requires every entry to be enclosed in quotes
# value: "{{ item.value | map('community.dns.quote_txt', always_quote=true) | list }}"
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
RETURN = '''
account_uri:
description: ACME account URI.
returned: success
type: str
validating_challenges:
description: List of challenges whose validation was triggered.
returned: success
type: list
elements: dict
contains:
identifier:
description:
- The identifier the challenge is for.
type: str
returned: always
identifier_type:
description:
- The identifier's type for the challenge.
type: str
returned: always
choices:
- dns
- ip
authz_url:
description:
- The URL of the authorization object for this challenge.
type: str
returned: always
challenge_type:
description:
- The challenge's type.
type: str
returned: always
choices:
- http-01
- dns-01
- tls-alpn-01
challenge_url:
description:
- The URL of the challenge object.
type: str
returned: always
'''
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
create_default_argspec,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.certificate import (
ACMECertificateClient,
)
def main():
argument_spec = create_default_argspec(with_certificate=False)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
challenge=dict(type='str', choices=['http-01', 'dns-01', 'tls-alpn-01']),
deactivate_authzs=dict(type='bool', default=True),
)
module = argument_spec.create_ansible_module()
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
done = False
order = None
try:
# Step 1: load order
order = client.load_order()
client.check_that_authorizations_can_be_used(order)
# Step 2: find all pending authorizations
pending_authzs = client.collect_pending_authzs(order)
# Step 3: figure out challenges to use
challenges = {}
for authz in pending_authzs:
challenges[authz.combined_identifier] = module.params['challenge']
missing_challenge_authzs = [k for k, v in challenges.items() if v is None]
if missing_challenge_authzs:
raise ModuleFailException(
'The challenge parameter must be supplied if there are pending authorizations.'
' The following authorizations are pending: {missing_challenge_authzs}'.format(
missing_challenge_authzs=', '.join(sorted(missing_challenge_authzs)),
)
)
bad_challenge_authzs = [
authz.combined_identifier for authz in pending_authzs
if authz.find_challenge(challenges[authz.combined_identifier]) is None
]
if bad_challenge_authzs:
raise ModuleFailException(
'The following authorizations do not support the selected challenges: {authz_challenges_pairs}'.format(
authz_challenges_pairs=', '.join(sorted(
'{authz} with {challenge}'.format(authz=authz, challenge=challenges[authz])
for authz in bad_challenge_authzs
)),
)
)
really_pending_authzs = [
authz for authz in pending_authzs
if authz.find_challenge(challenges[authz.combined_identifier]).status == 'pending'
]
# Step 4: validate pending authorizations
authzs_with_challenges_to_wait_for = client.call_validate(
really_pending_authzs,
get_challenge=lambda authz: challenges[authz.combined_identifier],
wait=False,
)
done = True
finally:
if order and module.params['deactivate_authzs'] and not done:
client.deactivate_authzs(order)
module.exit_json(
changed=len(authzs_with_challenges_to_wait_for) > 0,
account_uri=client.client.account_uri,
validating_challenges=[
dict(
identifier=authz.identifier,
identifier_type=authz.identifier_type,
authz_url=authz.url,
challenge_type=challenge_type,
challenge_url=challenge.url,
)
for authz, challenge_type, challenge in authzs_with_challenges_to_wait_for
],
)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
main()