From d906914737c40b60b23928f3929ff34d25d25c35 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 1 May 2024 10:32:03 +0200 Subject: [PATCH] Create acme_certificate_deactivate_authz module (#741) * Create acme_certificate_deactivate_authz module. * Add ACME version check. --- README.md | 1 + meta/runtime.yml | 1 + plugins/module_utils/acme/challenges.py | 10 +- plugins/modules/acme_certificate.py | 6 +- .../acme_certificate_deactivate_authz.py | 130 ++++++++++++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 plugins/modules/acme_certificate_deactivate_authz.py diff --git a/README.md b/README.md index 08b286c03..7e8136227 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ If you use the Ansible package and do not update collections independently, use - acme_account module - acme_ari_info module - acme_certificate module + - acme_certificate_deactivate_authz module - acme_certificate_revoke module - acme_challenge_cert_helper module - acme_inspect module diff --git a/meta/runtime.yml b/meta/runtime.yml index 76500748c..9c7592dc7 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -8,6 +8,7 @@ requires_ansible: '>=2.9.10' action_groups: acme: - acme_inspect + - acme_certificate_deactivate_authz - acme_certificate_revoke - acme_certificate - acme_account diff --git a/plugins/module_utils/acme/challenges.py b/plugins/module_utils/acme/challenges.py index 3a87ffec1..2b12c27c4 100644 --- a/plugins/module_utils/acme/challenges.py +++ b/plugins/module_utils/acme/challenges.py @@ -283,13 +283,21 @@ def call_validate(self, client, challenge_type, wait=True): return self.status == 'valid' return self.wait_for_validation(client, challenge_type) + def can_deactivate(self): + ''' + Deactivates this authorization. + https://community.letsencrypt.org/t/authorization-deactivation/19860/2 + https://tools.ietf.org/html/rfc8555#section-7.5.2 + ''' + return self.status in ('valid', 'pending') + def deactivate(self, client): ''' Deactivates this authorization. https://community.letsencrypt.org/t/authorization-deactivation/19860/2 https://tools.ietf.org/html/rfc8555#section-7.5.2 ''' - if self.status != 'valid': + if not self.can_deactivate(): return authz_deactivate = { 'status': 'deactivated' diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index 51be63ec7..94cb9bcd8 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -77,6 +77,8 @@ description: Allows to create, modify or delete an ACME account. - 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 @@ -309,7 +311,9 @@ 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. + 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 diff --git a/plugins/modules/acme_certificate_deactivate_authz.py b/plugins/modules/acme_certificate_deactivate_authz.py new file mode 100644 index 000000000..ffed04983 --- /dev/null +++ b/plugins/modules/acme_certificate_deactivate_authz.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Michael Gruener +# 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_deactivate_authz +author: "Felix Fontein (@felixfontein)" +version_added: 2.20.0 +short_description: Deactivate all authz for an ACME v2 order +description: + - "Deactivate all authentication objects (authz) for an ACME v2 order, + which effectively deactivates (invalidates) the order itself." + - "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." + - "Another reason to use this module is to deactivate an order whose + processing failed when using O(community.crypto.acme_certificate#module:include_renewal_cert_id)." +seealso: + - module: community.crypto.acme_certificate +extends_documentation_fragment: + - community.crypto.acme.basic + - community.crypto.acme.account + - community.crypto.attributes + - community.crypto.attributes.actiongroup_acme +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + order_uri: + description: + - The ACME v2 order to deactivate. + - Can be obtained from RV(community.crypto.acme_certificate#module:order_uri). + type: str + required: true +''' + +EXAMPLES = r''' +- name: Deactivate all authzs for an order + community.crypto.acme_certificate_deactivate_authz: + account_key_content: "{{ account_private_key }}" + order_uri: "{{ certificate_result.order_uri }}" +''' + +RETURN = '''#''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.acme.acme import ( + create_backend, + get_default_argspec, + ACMEClient, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.account import ( + ACMEAccount, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ( + ModuleFailException, +) + +from ansible_collections.community.crypto.plugins.module_utils.acme.orders import ( + Order, +) + + +def main(): + argument_spec = get_default_argspec() + argument_spec.update(dict( + order_uri=dict(type='str', required=True), + )) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=( + ['account_key_src', 'account_key_content'], + ), + mutually_exclusive=( + ['account_key_src', 'account_key_content'], + ), + 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 = ACMEClient(module, backend) + account = ACMEAccount(client) + + dummy, account_data = account.setup_account(allow_creation=False) + if account_data is None: + raise ModuleFailException(msg='Account does not exist or is deactivated.') + + order = Order.from_url(client, module.params['order_uri']) + order.load_authorizations(client) + + changed = False + for authz in order.authorizations.values(): + if not authz.can_deactivate(): + continue + changed = True + if module.check_mode: + continue + try: + authz.deactivate(client) + except Exception: + # ignore errors + pass + if authz.status != 'deactivated': + module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url)) + + module.exit_json(changed=changed) + except ModuleFailException as e: + e.do_fail(module) + + +if __name__ == '__main__': + main()