예제 #1
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            account_uri=dict(type='str'),
            acme_directory=dict(
                type='str',
                default='https://acme-staging.api.letsencrypt.org/directory'),
            acme_version=dict(type='int', default=1, choices=[1, 2]),
            validate_certs=dict(type='bool', default=True),
            select_crypto_backend=dict(
                type='str',
                default='auto',
                choices=['auto', 'openssl', 'cryptography']),
        ),
        required_one_of=(['account_key_src', 'account_key_content'], ),
        mutually_exclusive=(['account_key_src', 'account_key_content'], ),
        supports_check_mode=True,
    )
    set_crypto_backend(module)

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')
    if module.params.get('acme_version') < 2:
        module.fail_json(
            msg='The acme_account module requires the ACME v2 protocol!')

    try:
        account = ACMEAccount(module)
        # Check whether account exists
        created, account_data = account.setup_account(
            [],
            allow_creation=False,
            remove_account_uri_if_not_exists=True,
        )
        if created:
            raise AssertionError('Unwanted account creation')
        result = {
            'changed': False,
            'exists': account.uri is not None,
            'account_uri': account.uri,
        }
        if account.uri is not None:
            # Make sure promised data is there
            if 'contact' not in account_data:
                account_data['contact'] = []
            account_data['public_account_key'] = account.key_data['jwk']
            result['account'] = account_data
        module.exit_json(**result)
    except ModuleFailException as e:
        e.do_fail(module)
예제 #2
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            account_uri=dict(required=False, type='str'),
            modify_account=dict(required=False, type='bool', default=True),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            account_email=dict(required=False, default=None, type='str'),
            agreement=dict(required=False, type='str'),
            terms_agreed=dict(required=False, default=False, type='bool'),
            challenge=dict(required=False,
                           default='http-01',
                           choices=['http-01', 'dns-01', 'tls-alpn-01'],
                           type='str'),
            csr=dict(required=True, aliases=['src'], type='path'),
            data=dict(required=False, default=None, type='dict'),
            dest=dict(aliases=['cert'], type='path'),
            fullchain_dest=dict(aliases=['fullchain'], type='path'),
            chain_dest=dict(required=False,
                            default=None,
                            aliases=['chain'],
                            type='path'),
            remaining_days=dict(required=False, default=10, type='int'),
            deactivate_authzs=dict(required=False, default=False, type='bool'),
            force=dict(required=False, default=False, type='bool'),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        required_one_of=(
            ['account_key_src', 'account_key_content'],
            ['dest', 'fullchain_dest'],
        ),
        mutually_exclusive=(['account_key_src', 'account_key_content'], ),
        supports_check_mode=True,
    )
    if module._name == 'letsencrypt':
        module.deprecate(
            "The 'letsencrypt' module is being renamed 'acme_certificate'",
            version='2.10')
    set_crypto_backend(module)

    # AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
    module.run_command_environ_update = dict(LANG='C',
                                             LC_ALL='C',
                                             LC_MESSAGES='C',
                                             LC_CTYPE='C')
    locale.setlocale(locale.LC_ALL, 'C')

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')

    try:
        if module.params.get('dest'):
            cert_days = get_cert_days(module, module.params['dest'])
        else:
            cert_days = get_cert_days(module, module.params['fullchain_dest'])

        if module.params[
                'force'] or cert_days < module.params['remaining_days']:
            # If checkmode is active, base the changed state solely on the status
            # of the certificate file as all other actions (accessing an account, checking
            # the authorization status...) would lead to potential changes of the current
            # state
            if module.check_mode:
                module.exit_json(changed=True,
                                 authorizations={},
                                 challenge_data={},
                                 cert_days=cert_days)
            else:
                client = ACMEClient(module)
                client.cert_days = cert_days
                if client.is_first_step():
                    # First run: start challenges / start new order
                    client.start_challenges()
                else:
                    # Second run: finish challenges, and get certificate
                    try:
                        client.finish_challenges()
                        client.get_certificate()
                    finally:
                        if module.params['deactivate_authzs']:
                            client.deactivate_authzs()
                data, data_dns = client.get_challenges_data()
                module.exit_json(changed=client.changed,
                                 authorizations=client.authorizations,
                                 finalize_uri=client.finalize_uri,
                                 order_uri=client.order_uri,
                                 account_uri=client.account.uri,
                                 challenge_data=data,
                                 challenge_data_dns=data_dns,
                                 cert_days=client.cert_days)
        else:
            module.exit_json(changed=False, cert_days=cert_days)
    except ModuleFailException as e:
        e.do_fail(module)
예제 #3
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            account_uri=dict(required=False, type='str'),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            private_key_src=dict(type='path'),
            private_key_content=dict(type='str', no_log=True),
            certificate=dict(required=True, type='path'),
            revoke_reason=dict(required=False, type='int'),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        required_one_of=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        mutually_exclusive=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        supports_check_mode=False,
    )
    set_crypto_backend(module)

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')

    try:
        account = ACMEAccount(module)
        # Load certificate
        certificate = pem_to_der(module.params.get('certificate'))
        certificate = nopad_b64(certificate)
        # Construct payload
        payload = {'certificate': certificate}
        if module.params.get('revoke_reason') is not None:
            payload['reason'] = module.params.get('revoke_reason')
        # Determine endpoint
        if module.params.get('acme_version') == 1:
            endpoint = account.directory['revoke-cert']
            payload['resource'] = 'revoke-cert'
        else:
            endpoint = account.directory['revokeCert']
        # Get hold of private key (if available) and make sure it comes from disk
        private_key = module.params.get('private_key_src')
        private_key_content = module.params.get('private_key_content')
        # Revoke certificate
        if private_key or private_key_content:
            # Step 1: load and parse private key
            error, private_key_data = account.parse_key(
                private_key, private_key_content)
            if error:
                raise ModuleFailException(
                    "error while parsing private key: %s" % error)
            # Step 2: sign revokation request with private key
            jws_header = {
                "alg": private_key_data['alg'],
                "jwk": private_key_data['jwk'],
            }
            result, info = account.send_signed_request(
                endpoint,
                payload,
                key_data=private_key_data,
                jws_header=jws_header)
        else:
            # Step 1: get hold of account URI
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            # Step 2: sign revokation request with account key
            result, info = account.send_signed_request(endpoint, payload)
        if info['status'] != 200:
            already_revoked = False
            # Standarized error in draft 14 (https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.6)
            if result.get(
                    'type') == 'urn:ietf:params:acme:error:alreadyRevoked':
                already_revoked = True
            else:
                # Hack for Boulder errors
                if module.params.get('acme_version') == 1:
                    error_type = 'urn:acme:error:malformed'
                else:
                    error_type = 'urn:ietf:params:acme:error:malformed'
                if result.get('type') == error_type and result.get(
                        'detail') == 'Certificate already revoked':
                    # Fallback: boulder returns this in case the certificate was already revoked.
                    already_revoked = True
            # If we know the certificate was already revoked, we don't fail,
            # but successfully terminate while indicating no change
            if already_revoked:
                module.exit_json(changed=False)
            raise ModuleFailException(
                'Error revoking certificate: {0} {1}'.format(
                    info['status'], result))
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)
예제 #4
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            terms_agreed=dict(required=False, default=False, type='bool'),
            state=dict(required=True,
                       choices=['absent', 'present', 'changed_key'],
                       type='str'),
            allow_creation=dict(required=False, default=True, type='bool'),
            contact=dict(required=False, type='list', default=[]),
            new_account_key_src=dict(type='path'),
            new_account_key_content=dict(type='str', no_log=True),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        required_one_of=(['account_key_src', 'account_key_content'], ),
        mutually_exclusive=(
            ['account_key_src', 'account_key_content'],
            ['new_account_key_src', 'new_account_key_content'],
        ),
        required_if=(
            # Make sure that for state == changed_key, one of
            # new_account_key_src and new_account_key_content are specified
            [
                'state', 'changed_key',
                ['new_account_key_src', 'new_account_key_content'], True
            ], ),
        supports_check_mode=True,
    )
    set_crypto_backend(module)

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')
    if module.params.get('acme_version') < 2:
        module.fail_json(
            msg='The acme_account module requires the ACME v2 protocol!')

    try:
        account = ACMEAccount(module)
        state = module.params.get('state')
        if state == 'absent':
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            if account.uri is not None:
                # Account does exist
                account_data = account.get_account_data()
                if account_data is not None:
                    # Account is not yet deactivated
                    if not module.check_mode:
                        # Deactivate it
                        payload = {'status': 'deactivated'}
                        result, info = account.send_signed_request(
                            account.uri, payload)
                        if info['status'] != 200:
                            raise ModuleFailException(
                                'Error deactivating account: {0} {1}'.format(
                                    info['status'], result))
                    module.exit_json(changed=True, account_uri=account.uri)
            module.exit_json(changed=False, account_uri=account.uri)
        elif state == 'present':
            allow_creation = module.params.get('allow_creation')
            contact = module.params.get('contact')
            terms_agreed = module.params.get('terms_agreed')
            changed = account.init_account(
                contact,
                terms_agreed=terms_agreed,
                allow_creation=allow_creation,
            )
            if account.uri is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            module.exit_json(changed=changed, account_uri=account.uri)
        elif state == 'changed_key':
            # Parse new account key
            error, new_key_data = account.parse_key(
                module.params.get('new_account_key_src'),
                module.params.get('new_account_key_content'))
            if error:
                raise ModuleFailException(
                    "error while parsing account key: %s" % error)
            # Verify that the account exists and has not been deactivated
            changed = account.init_account(
                [],
                allow_creation=False,
                update_contact=False,
            )
            if changed:
                raise AssertionError('Unwanted account change')
            if account.uri is None or account.get_account_data() is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            # Now we can start the account key rollover
            if not module.check_mode:
                # Compose inner signed message
                # https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.3.6
                url = account.directory['keyChange']
                protected = {
                    "alg": new_key_data['alg'],
                    "jwk": new_key_data['jwk'],
                    "url": url,
                }
                payload = {
                    "account": account.uri,
                    "newKey": new_key_data['jwk'],  # specified in draft 12
                    "oldKey": account.
                    jwk,  # discussed in https://github.com/ietf-wg-acme/acme/pull/425,
                    # might be required in draft 13
                }
                data = account.sign_request(protected, payload, new_key_data)
                # Send request and verify result
                result, info = account.send_signed_request(url, data)
                if info['status'] != 200:
                    raise ModuleFailException(
                        'Error account key rollover: {0} {1}'.format(
                            info['status'], result))
            module.exit_json(changed=True, account_uri=account.uri)
    except ModuleFailException as e:
        e.do_fail(module)
예제 #5
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            account_uri=dict(required=False, type='str'),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            url=dict(required=False, type='str'),
            method=dict(required=False,
                        type='str',
                        choices=['get', 'post', 'directory-only'],
                        default='get'),
            content=dict(required=False, type='str'),
            fail_on_acme_error=dict(required=False, type='bool', default=True),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        mutually_exclusive=(['account_key_src', 'account_key_content'], ),
        required_if=(
            ['method', 'get', ['url']],
            ['method', 'post', ['url', 'content']],
            [
                'method', 'get', ['account_key_src', 'account_key_content'],
                True
            ],
            [
                'method', 'post', ['account_key_src', 'account_key_content'],
                True
            ],
        ),
    )
    set_crypto_backend(module)

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')

    result = dict()
    changed = False
    try:
        # Get hold of ACMEAccount object (includes directory)
        account = ACMEAccount(module)
        method = module.params['method']
        result['directory'] = account.directory.directory
        # Do we have to do more requests?
        if method != 'directory-only':
            url = module.params['url']
            fail_on_acme_error = module.params['fail_on_acme_error']
            # Do request
            if method == 'get':
                data, info = account.get_request(url,
                                                 parse_json_result=False,
                                                 fail_on_error=False)
            elif method == 'post':
                changed = True  # only POSTs can change
                data, info = account.send_signed_request(
                    url,
                    to_bytes(module.params['content']),
                    parse_json_result=False,
                    encode_payload=False)
            # Update results
            result.update(dict(
                headers=info,
                output_text=to_native(data),
            ))
            # See if we can parse the result as JSON
            try:
                result['output_json'] = json.loads(data)
            except Exception as dummy:
                pass
            # Fail if error was returned
            if fail_on_acme_error and info['status'] >= 400:
                raise ModuleFailException(
                    "ACME request failed: CODE: {0} RESULT: {1}".format(
                        info['status'], data))
        # Done!
        module.exit_json(changed=changed, **result)
    except ModuleFailException as e:
        e.do_fail(module, **result)
예제 #6
0
def main():
    module = AnsibleModule(
        argument_spec=dict(
            account_key_src=dict(type='path', aliases=['account_key']),
            account_key_content=dict(type='str', no_log=True),
            account_uri=dict(required=False, type='str'),
            acme_directory=dict(
                required=False,
                default='https://acme-staging.api.letsencrypt.org/directory',
                type='str'),
            acme_version=dict(required=False,
                              default=1,
                              choices=[1, 2],
                              type='int'),
            validate_certs=dict(required=False, default=True, type='bool'),
            terms_agreed=dict(required=False, default=False, type='bool'),
            state=dict(required=True,
                       choices=['absent', 'present', 'changed_key'],
                       type='str'),
            allow_creation=dict(required=False, default=True, type='bool'),
            contact=dict(required=False,
                         type='list',
                         elements='str',
                         default=[]),
            new_account_key_src=dict(type='path'),
            new_account_key_content=dict(type='str', no_log=True),
            select_crypto_backend=dict(
                required=False,
                choices=['auto', 'openssl', 'cryptography'],
                default='auto',
                type='str'),
        ),
        required_one_of=(['account_key_src', 'account_key_content'], ),
        mutually_exclusive=(
            ['account_key_src', 'account_key_content'],
            ['new_account_key_src', 'new_account_key_content'],
        ),
        required_if=(
            # Make sure that for state == changed_key, one of
            # new_account_key_src and new_account_key_content are specified
            [
                'state', 'changed_key',
                ['new_account_key_src', 'new_account_key_content'], True
            ], ),
        supports_check_mode=True,
    )
    set_crypto_backend(module)

    if not module.params.get('validate_certs'):
        module.warn(
            warning=
            'Disabling certificate validation for communications with ACME endpoint. '
            +
            'This should only be done for testing against a local ACME server for '
            + 'development purposes, but *never* for production purposes.')
    if module.params.get('acme_version') < 2:
        module.fail_json(
            msg='The acme_account module requires the ACME v2 protocol!')

    try:
        account = ACMEAccount(module)
        changed = False
        state = module.params.get('state')
        diff_before = {}
        diff_after = {}
        if state == 'absent':
            created, account_data = account.setup_account(allow_creation=False)
            if account_data:
                diff_before = dict(account_data)
                diff_before['public_account_key'] = account.key_data['jwk']
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is not None:
                # Account is not yet deactivated
                if not module.check_mode:
                    # Deactivate it
                    payload = {'status': 'deactivated'}
                    result, info = account.send_signed_request(
                        account.uri, payload)
                    if info['status'] != 200:
                        raise ModuleFailException(
                            'Error deactivating account: {0} {1}'.format(
                                info['status'], result))
                changed = True
        elif state == 'present':
            allow_creation = module.params.get('allow_creation')
            # Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us)
            contact = [str(v) for v in module.params.get('contact')]
            terms_agreed = module.params.get('terms_agreed')
            created, account_data = account.setup_account(
                contact,
                terms_agreed=terms_agreed,
                allow_creation=allow_creation,
            )
            if account_data is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            if created:
                diff_before = {}
            else:
                diff_before = dict(account_data)
                diff_before['public_account_key'] = account.key_data['jwk']
            updated = False
            if not created:
                updated, account_data = account.update_account(
                    account_data, contact)
            changed = created or updated
            diff_after = dict(account_data)
            diff_after['public_account_key'] = account.key_data['jwk']
        elif state == 'changed_key':
            # Parse new account key
            error, new_key_data = account.parse_key(
                module.params.get('new_account_key_src'),
                module.params.get('new_account_key_content'))
            if error:
                raise ModuleFailException(
                    "error while parsing account key: %s" % error)
            # Verify that the account exists and has not been deactivated
            created, account_data = account.setup_account(allow_creation=False)
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            diff_before = dict(account_data)
            diff_before['public_account_key'] = account.key_data['jwk']
            # Now we can start the account key rollover
            if not module.check_mode:
                # Compose inner signed message
                # https://tools.ietf.org/html/draft-ietf-acme-acme-14#section-7.3.6
                url = account.directory['keyChange']
                protected = {
                    "alg": new_key_data['alg'],
                    "jwk": new_key_data['jwk'],
                    "url": url,
                }
                payload = {
                    "account": account.uri,
                    "newKey":
                    new_key_data['jwk'],  # specified in draft 12 and older
                    "oldKey": account.jwk,  # specified in draft 13 and newer
                }
                data = account.sign_request(protected, payload, new_key_data)
                # Send request and verify result
                result, info = account.send_signed_request(url, data)
                if info['status'] != 200:
                    raise ModuleFailException(
                        'Error account key rollover: {0} {1}'.format(
                            info['status'], result))
                if module._diff:
                    account.key_data = new_key_data
                    account.jws_header['alg'] = new_key_data['alg']
                    diff_after = account.get_account_data()
            elif module._diff:
                # Kind of fake diff_after
                diff_after = dict(diff_before)
            diff_after['public_account_key'] = new_key_data['jwk']
            changed = True
        result = {
            'changed': changed,
            'account_uri': account.uri,
        }
        if module._diff:
            result['diff'] = {
                'before': diff_before,
                'after': diff_after,
            }
        module.exit_json(**result)
    except ModuleFailException as e:
        e.do_fail(module)