Repository: ansible/ansible

Issue:
Letsencrypt: Not creating certificates
#### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
letsencrypt

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
Linux 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

##### SUMMARY
The letsencrypt module in 2.5 only creates empty certificate files when renewing a certificate. This works fine in 2.4.3.

##### STEPS TO REPRODUCE
With Ansible version 2.5, run the below playbook to renew a Letsencrypt certificate with dns-01 challenge.

```yaml
- hosts: 127.0.0.1
  tasks:
    - name: Renew LetsEncrypt certificates
      letsencrypt:
        account_key: /letsencrypt/letsencrypt-account.key
        csr: /letsencrypt/{{ item }}.csr
        dest: "/etc/ssl/{{ item }}.crt"
        acme_directory: https://acme-v01.api.letsencrypt.org/directory
      with_items: "{{ ssl_certs }}"
  vars:
      ssl_certs:
        - www.example.com
```


##### EXPECTED RESULTS
`/etc/ssl/www.example.com.crt` is created with as a valid certificate

Verbose results from 2.4.3.0:
```
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/web_infrastructure/letsencrypt.py
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153 `" && echo ansible-tmp-1521935449.41-132763795794153="` echo /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpLXd6we TO /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153/letsencrypt.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153/ /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153/letsencrypt.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153/letsencrypt.py; rm -rf "/home/user/.ansible/tmp/ansible-tmp-1521935449.41-132763795794153/" > /dev/null 2>&1 && sleep 0'
changed: [127.0.0.1] => (item=www.example.com) => {
    "authorizations": [
        {
            "challenges": [
                {
                    "status": "pending", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "http-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz"
                }, 
                {
                    "keyAuthorization": "abcdefghijklmnopqrstuvwxyz", 
                    "status": "valid", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "dns-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz", 
                    "validationRecord": [
                        {
                            "hostname": "www.example.com"
                        }
                    ]
                }
            ], 
            "combinations": [
                [
                    1
                ], 
                [
                    0
                ]
            ], 
            "expires": "2018-04-11T01:26:27Z", 
            "identifier": {
                "type": "dns", 
                "value": "www.example.com"
            }, 
            "status": "valid", 
            "uri": "https://acme-v01.api.letsencrypt.org/acme/authz/abcdefghijklmnopqrstuvwxyz"
        }, 
        {
            "challenges": [
                {
                    "keyAuthorization": "abcdefghijklmnopqrstuvwxyz", 
                    "status": "valid", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "dns-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz", 
                    "validationRecord": [
                        {
                            "hostname": "example.com"
                        }
                    ]
                }, 
                {
                    "status": "pending", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "http-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz"
                }
            ], 
            "combinations": [
                [
                    0
                ], 
                [
                    1
                ]
            ], 
            "expires": "2018-04-11T01:26:31Z", 
            "identifier": {
                "type": "dns", 
                "value": "example.com"
            }, 
            "status": "valid", 
            "uri": "https://acme-v01.api.letsencrypt.org/acme/authz/abcdefghijklmnopqrstuvwxyz"
        }
    ], 
    "cert_days": 89, 
    "challenge_data": {
        "example.com": {
            "dns-01": {
                "resource": "_acme-challenge", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }, 
            "http-01": {
                "resource": ".well-known/acme-challenge/abcdefghijklmnopqrstuvwxyz", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }
        }, 
        "www.example.com": {
            "dns-01": {
                "resource": "_acme-challenge", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }, 
            "http-01": {
                "resource": ".well-known/acme-challenge/abcdefghijklmnopqrstuvwxyz", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }
        }
    }, 
    "changed": true, 
    "invocation": {
        "module_args": {
            "account_email": null, 
            "account_key": "/letsencrypt/letsencrypt-account.key", 
            "acme_directory": "https://acme-v01.api.letsencrypt.org/directory", 
            "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", 
            "challenge": "http-01", 
            "csr": "/letsencrypt/www.example.com.csr", 
            "data": null, 
            "dest": "/etc/ssl/www.example.com.crt", 
            "remaining_days": 10
        }
    }, 
    "item": "www.example.com"
}

```

##### ACTUAL RESULTS
`/etc/ssl/www.example.com.crt` not created

Verbose results from 2.5
```
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/web_infrastructure/letsencrypt.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: root
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896 `" && echo ansible-tmp-1521934672.46-231150476528896="` echo /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896 `" ) && sleep 0'
<127.0.0.1> PUT /home/user/.ansible/tmp/ansible-local-9443jIZoQC/tmpnTgh6s TO /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896/letsencrypt.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896/ /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896/letsencrypt.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896/letsencrypt.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /home/user/.ansible/tmp/ansible-tmp-1521934672.46-231150476528896/ > /dev/null 2>&1 && sleep 0'
changed: [127.0.0.1] => (item=www.example.com) => {
    "account_uri": "https://acme-v01.api.letsencrypt.org/acme/reg/30973482", 
    "authorizations": {
        "www.example.com": {
            "challenges": [
                {
                    "keyAuthorization": "abcdefghijklmnopqrstuvwxyz", 
                    "status": "valid", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "dns-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz", 
                    "validationRecord": [
                        {
                            "hostname": "www.example.com"
                        }
                    ]
                }, 
                {
                    "status": "pending", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "http-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz"
                }
            ], 
            "combinations": [
                [
                    1
                ], 
                [
                    0
                ]
            ], 
            "expires": "2018-04-23T16:02:57Z", 
            "identifier": {
                "type": "dns", 
                "value": "www.example.com"
            }, 
            "status": "valid", 
            "uri": "https://acme-v01.api.letsencrypt.org/acme/authz/abcdefghijklmnopqrstuvwxyz"
        }, 
        "example": {
            "challenges": [
                {
                    "keyAuthorization": "abcdefghijklmnopqrstuvwxyz", 
                    "status": "valid", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "dns-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz", 
                    "validationRecord": [
                        {
                            "hostname": "example.com"
                        }
                    ]
                }, 
                {
                    "status": "pending", 
                    "token": "abcdefghijklmnopqrstuvwxyz", 
                    "type": "http-01", 
                    "uri": "https://acme-v01.api.letsencrypt.org/acme/challenge/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz"
                }
            ], 
            "combinations": [
                [
                    1
                ], 
                [
                    0
                ]
            ], 
            "expires": "2018-04-11T03:03:41Z", 
            "identifier": {
                "type": "dns", 
                "value": "example.com"
            }, 
            "status": "valid", 
            "uri": "https://acme-v01.api.letsencrypt.org/acme/authz/abcdefghijklmnopqrstuvwxyz"
        }
    }, 
    "cert_days": -1, 
    "challenge_data": {
        "www.example.com": {
            "dns-01": {
                "record": "_acme-challenge.www.example.com", 
                "resource": "_acme-challenge", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }, 
            "http-01": {
                "resource": ".well-known/acme-challenge/abcdefghijklmnopqrstuvwxyz", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }
        }, 
        "example.com": {
            "dns-01": {
                "record": "_acme-challenge.example.com", 
                "resource": "_acme-challenge", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }, 
            "http-01": {
                "resource": ".well-known/acme-challenge/abcdefghijklmnopqrstuvwxyz", 
                "resource_value": "abcdefghijklmnopqrstuvwxyz"
            }
        }
    }, 
    "challenge_data_dns": {}, 
    "changed": true, 
    "finalize_uri": null, 
    "invocation": {
        "module_args": {
            "account_email": null, 
            "account_key": "/letsencrypt/letsencrypt-account.key", 
            "account_key_content": null, 
            "account_key_src": "/letsencrypt/letsencrypt-account.key", 
            "acme_directory": "https://acme-v01.api.letsencrypt.org/directory", 
            "acme_version": 1, 
            "agreement": null, 
            "chain_dest": null, 
            "challenge": "http-01", 
            "csr": "/letsencrypt/www.example.com.csr", 
            "data": null, 
            "dest": "/etc/ssl/www.example.com.crt", 
            "fullchain_dest": null, 
            "remaining_days": 10, 
            "terms_agreed": false, 
            "validate_certs": true
        }
    }, 
    "item": "www.example.com", 
    "order_uri": null
}
```

_Data scrubbed for security_


Discussion:
Files identified in the description:
* [lib/ansible/modules/web_infrastructure/letsencrypt.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/letsencrypt.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @felixfontein @mgruener @resmo
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@apple4ever Could you please post a more complete use of the `letsencrypt` module? You are simply doing the first invocation (also not with `dns-01` challenge), i.e. preparing the challenges, but you don't invoke it a second time to actually get a certificate. Only in this second step (where the `data` option is provided)), the module is writing to the certificate file.
!needs_info
needs_info
Ah sorry I didn't see your question.

I'm not sure... I copied the verbose output from the same commands (the simplified playbook I put above) on 2.5 and 2.4.3, the former which does actually create the file but its just empty and the later which creates the file with the certificate. The command is not preparing the challenges, but actually requesting the certificate. I do the challenge off that server manually.
If you look at the `cert_days` key, you can see the difference between the two versions. Not sure if that helps. 
You need to pass the output of the first invocation into the second invocation as `data`. If you don't do that, the second invocation doesn't know it is the second invocation, and behaves like the first invocation.(*)

Since you called the module without `data`, it assumes it is the first invocation, and does *not* write to the certificate file. So the invocation should not touch the certificate file. Also, on the fist invocation, `cert_days` returned is the number of days the old certificate is valid, which is -1 in your case. The module should return -1 only if the certificate file does not exist (or that the certificate expired on the day before). I don't know who created the empty file, but it should not have been the `letsencrypt` module. At least, I have no idea how this could have potentially happened...


(*) This is slightly different from the 2.4.x version, which allowed not to pass the data. I changed this as part of 2ebb611b502d56cb91f7e33314e827f580be1ada as the old behavior made it impossible for a playbook to determine whether the certificate was already issued in the very first invocation (this could happen since Let's Encrypt stored successful authz for a certain amount of time). For me, this caused certificates to be issued *twice* (one on each invocation), each of which shows up in the Certificate Transparency logs and each of which counts against the rate limits.
Unfortunately, this change prevents your usecase, where you simply call the module twice without setting `data` in the second case. (It is also not possible to add a fake `data` record to the second invocation, since the module now assumes that it contains at least the authorization data.) I wasn't aware that someone is really doing this. Sorry!

That's definitely a change from the way its working, and I understand what is happening. But I still don't understand why it needs `data`. Let's Encrypt doesn't require ongoing authorization past the initial issuance, so there shouldn't be a need to require `data`. A better way would be to be able to tell the module "just issue a certificate" for the second run if `data` is not defined. I suppose I could try to dive into the code and submit MR for that. 

But a better error message would be helpful here, as well as the docs marking `data` for the second run. 

I will try to capture the data from the first run when I manually do it, and see if that will allow it to do what its supposed to do.
Well, it does require regular re-authorization after the first issuance. The authorization data is cached for a certain amount of time (I think currently 6 months, with plans to reduce this); after that you definitely need to re-authorize every domain.

Also, there's a difference on how ACME v1 and ACME v2 are handling this. For ACME v1, you create an authorization (authz) for every domain of the certificate in the first step (i.e. first invocation of `letsencrypt` module), then fulfill one of their challenges (one for each authz), and then in the second step (second invocation of `letsencrypt`) you tell the ACME endpoint to validate these challenges, and to issue a certificate for them if everything went smoothly. For ACME v1, this second step can be done by asking again for authz objects for the same domains; the way boulder (the ACME v1 endpoint software by Let's Encrypt) is implemented is that you get the the same authz objects if you ask again (and they didn't expire since the last time you asked), so here you don't need any state. It's not a clean use of the protocol, from my point of view, but possible to do this.

For ACME v2, the procedure is different. The protocol still allows for pre-authorization (https://tools.ietf.org/html/draft-ietf-acme-acme-11#section-7.4.1), but boulder currently doesn't implement that (I don't know if there are plans to actually do that). The [regular process](https://tools.ietf.org/html/draft-ietf-acme-acme-11#section-7.4) is to start a new order for a set of domains, which gives you an order URL. From there you can get a list of challenges you have to satisfy (you get a list of authz objects, to be more precise, each of them having a list of challenges). In the second step (second invocation of `letsencrypt`), you have to (as before) tell the ACME v2 endpoint to validate the challenges, and then finalize the order. For that, you need the order URL (or the finalization URL contained in the order object).

In the specs, there is a way to obtain a list of all open orders, but that's not implemented yet; so if you don't have `data` you cannot do this (for now). Another way would be to start a new order for the same set of domains and *hope* that you either get the same order URL, or that at least the new order object will have the same authz object than the previous order. If that is the case, you can simply continue with the new order object (since you already satisfied the challenges). The current implementation of boulder will give you the same authz objects (not sure about the same order, I guess you would have to test it or check the sources), so it will work. I don't think this is specified, though, so a later version might not do this anymore, and a different implementation of the ACME protocol might not do that either. That's why for ACME v2, I think we really need `data`, at least for now. (You only really need the order URL, though, not the authorization data.)

Regarding the error message you specify: on which condition should an error be given? The module doesn't know whether it is invoced a second time or not if `data` isn't specified.

Regarding the documentation: the documentation should be indeed more clearer. I thought it was already mentioning this, but it clearly isn't. The documentation for `data` itself says "The value that must be used here will be provided by a previous use of this module.", and the examples show how the data is passed, but it would definitely be good if in the module's description it would say that for the second invocation, you need the output of the first.

Regarding your use-case: I guess you can write the captured output into a file, and then read that during your second playbook. That shouldn't be too hard to implement. What you could also do is put everything into a single playbook, and put a [pause](https://docs.ansible.com/ansible/latest/modules/pause_module.html) command between the two `letsencrypt` invocations. Then you can resolve the challenges manually inbetween, and continue once you fulfilled them.
Thanks for the detailed answers. Bunch of issues here.

First, I'm dumb, a later task was touching the file to ensure proper permissions, so that's why it was creating an empty file. DOH. I renamed this issue to be more clear of the actual issue.

Second, part of this issue is the Let's Encrypt documentation isn't clear that authorization will need to happen every time, and all the instructions I found for the Ansible module remove the TXT records. I thought that meant it was a onetime issue. So I was trying to avoid a complex playbook on the server. Looks like I'll just completely redo the way I do it now.

Third, yes the documentation for the letsencrypt module isn't clear that `data` is required now (and I think this is really the crux of the issue here), though there is a hint about that as you noted. I know marking it as "required" isn't entirely accurate because on the first run it isn't. Maybe a different parameter to specify the different runs? For the first run, `submit_authorization` or `submit_challenge` would be true, and if `data` is defined, error out because its the first run (and likely a user mistake). For the second run, `data` would need to be defined and error out if its not defined. This would provide a better differentiation between the runs. Or maybe there should be two modules instead of one, as successive runs just seems off. `letsencrypt_challenges` & `letsencrypt_certificates`?

Oh, just FYI I do automate the adding of the TXT records, it was just a separately playbook I ran once for each server. Like I said, I'll have to just integrate that into the server cron playbook now.
I'm somewhat glad that you found the issue and it wasn't inside the `letsencrypt` module; that would have been really bad :)

About the module documentation: I've tried to make it clearer in #38135. Please take a look and feel free to add criticism, suggestions, further improvements, ...

About adding new arguments or splitting up the module: I'm not sure whether that's a good idea. I think I'd prefer adding new arguments over splitting the module into two for several reasons (one being that the documentation would be split over two modules, which I think would make it harder to understand in the first place). I think that the documentation (at least an improved version of it :) ) should/would be clear enough to prevent this. @resmo, what do you think?

Ok, the documentation part of this is resolved. It will show up in Ansible 2.5.1.
Files identified in the description:
* [lib/ansible/modules/web_infrastructure/_letsencrypt.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/_letsencrypt.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
!component +lib/ansible/modules/web_infrastructure/acme_certificate.py
Files identified in the description:
* [lib/ansible/modules/web_infrastructure/_letsencrypt.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/_letsencrypt.py)
* [lib/ansible/modules/web_infrastructure/acme_certificate.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/acme_certificate.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
!component -lib/ansible/modules/web_infrastructure/_letsencrypt.py
Files identified in the description:
* [lib/ansible/modules/web_infrastructure/acme_certificate.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/acme_certificate.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
-label deprecated
bot_status
### Components
[lib/ansible/modules/web_infrastructure/acme_certificate.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/web_infrastructure/acme_certificate.py)
support: community
maintainers: felixfontein mgruener resmo

### Metadata
**waiting_on**: maintainer
[needs_info](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md#label-needs_info): False

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: bot_status --->
Soooooo as I'm here trying to rewrite this to work, I realized that I got the idea from you:

https://github.com/felixfontein/acme-certificate

Looking back, I didn't realize that it would still need to be run on a schedule to keep the certs updated!

Anyway, are we just going to leave it as is with the improved documentation? Or do we want to further work on how it operates?
Nice to know that ;-)

Anyway, I don't mind to keep it the current way (with the improved docs), at least for now.

What do you think about the following:
 * (for ACME v2) add new parameter `order_uri` (string), which defaults to `None`, and if provided, means that we're in the second stage and all data is taken from the order URI;
 * (for ACME v1) add new parameter `second_stage` (bool), which defaults to `False`, and if provided, means that we're in the second stage and that all data is fetched based on the CSR.
The three parameters `data`, `order_uri` and `second_stage` would be mutually exclusive, and `data` would get deprecated.

The big downside is that we have two more arguments, which will make the documentation much more complicated. (Also, I would probably drop ACME v1 support in https://github.com/felixfontein/acme-certificate because that change would make it too annoying to support both. But then, ACME v2 is the future anyway. Even though most non-Let's-Encrypt ACME endpoints don't support v2 yet...)

Maybe a better short-term goal is to reduce the dependency on the data provided in `data`; so for ACME v2, `data.order_uri` must be there and nothing else is used, and for ACME v1, `data` must just be non-empty (or something like that) to indicate stage two.
I think the short term goal for now to bridge until ACMEv1 is deprecated. I think long term (assuming nothing changes) using `order_uri` for v2 to differentiate is the way to go.

Resolving patch:
diff --git a/lib/ansible/modules/web_infrastructure/acme_certificate.py b/lib/ansible/modules/web_infrastructure/acme_certificate.py
--- a/lib/ansible/modules/web_infrastructure/acme_certificate.py
+++ b/lib/ansible/modules/web_infrastructure/acme_certificate.py
@@ -97,6 +97,10 @@
          the second run of the module only."
       - "The value that must be used here will be provided by a previous use
          of this module. See the examples for more details."
+      - "Note that for ACME v2, only the C(order_uri) entry of C(data) will
+         be used. For ACME v1, C(data) must be non-empty to indicate the
+         second stage is active; all needed data will be taken from the
+         CSR."
       - "I(Note): the C(data) option was marked as C(no_log) up to
          Ansible 2.5. From Ansible 2.6 on, it is no longer marked this way
          as it causes error messages to be come unusable, and C(data) does
@@ -363,7 +367,7 @@ def __init__(self, module):
         self.authorizations = None
         self.cert_days = -1
         self.order_uri = self.data.get('order_uri') if self.data else None
-        self.finalize_uri = self.data.get('finalize_uri') if self.data else None
+        self.finalize_uri = None
 
         # Make sure account exists
         modify_account = module.params['modify_account']
@@ -679,11 +683,15 @@ def is_first_step(self):
         Return True if this is the first execution of this module, i.e. if a
         sufficient data object from a first run has not been provided.
         '''
-        if (self.data is None) or ('authorizations' not in self.data):
+        if self.data is None:
             return True
-        if self.finalize_uri is None and self.version != 1:
-            return True
-        return False
+        if self.version == 1:
+            # As soon as self.data is a non-empty object, we are in the second stage.
+            return not self.data
+        else:
+            # We are in the second stage if data.order_uri is given (which has been
+            # stored in self.order_uri by the constructor).
+            return self.order_uri is None
 
     def start_challenges(self):
         '''
@@ -725,8 +733,42 @@ def finish_challenges(self):
         Verify challenges for all domains of the CSR.
         '''
         self.authorizations = {}
-        for domain, auth in self.data['authorizations'].items():
-            self.authorizations[domain] = auth
+
+        # Step 1: obtain challenge information
+        if self.version == 1:
+            # For ACME v1, we attempt to create new authzs. Existing ones
+            # will be returned instead.
+            for domain in self.domains:
+                new_auth = self._new_authz_v1(domain)
+                self._add_or_update_auth(domain, new_auth)
+        else:
+            # For ACME v2, we obtain the order object by fetching the
+            # order URI, and extract the information from there.
+            resp, info = fetch_url(self.module, self.order_uri)
+            try:
+                result = resp.read()
+            except AttributeError:
+                result = info.get('body')
+
+            if not result:
+                raise ModuleFailException("Cannot download order from {0}: {1} (headers: {2})".format(self.order_uri, result, info))
+
+            if info['status'] not in [200]:
+                raise ModuleFailException("Error on downloading order: CODE: {0} RESULT: {1}".format(info['status'], result))
+
+            result = self.module.from_json(result.decode('utf8'))
+            for auth_uri in result['authorizations']:
+                auth_data = simple_get(self.module, auth_uri)
+                auth_data['uri'] = auth_uri
+                domain = auth_data['identifier']['value']
+                if auth_data.get('wildcard', False):
+                    domain = '*.{0}'.format(domain)
+                self.authorizations[domain] = auth_data
+
+            self.finalize_uri = result['finalize']
+
+        # Step 2: validate challenges
+        for domain, auth in self.authorizations.items():
             if auth['status'] == 'pending':
                 self._validate_challenges(domain, auth)
 


Test patch: