Repository: certbot/certbot

Issue:
Certbot-dns-cloudflare 1.30.0 doesn't work with cloudflare 2.10.1
If you're having trouble using Certbot and aren't sure you've found a bug or
request for a new feature, please first try asking for help at
https://community.letsencrypt.org/. There is a much larger community there of
people familiar with the project who will be able to more quickly answer your
questions.

## My operating system is (include version): Gentoo Linux


## I installed Certbot with (snap, OS package manager, pip, certbot-auto, etc):
package manager

## I ran this command and it produced this output:
certbot renew --dry-run -v
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/phoenix591.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator dns-cloudflare, Installer None
Simulating renewal of an existing certificate for *.phoenix591.com and phoenix591.com
Performing the following challenges:
dns-01 challenge for phoenix591.com
dns-01 challenge for phoenix591.com
Cleaning up challenges
Failed to renew certificate phoenix591.com with error: Unable to determine zone_id for phoenix591.com using zone names: ['phoenix591.com', 'com']. The error from Cloudflare was: 0 confused info - both key and token defined.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
All simulated renewals failed. The following certificates could not be renewed:
  /etc/letsencrypt/live/phoenix591.com/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 renew failure(s), 0 parse failure(s)
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.

Downgrading cloudflare to 2.9.10 fixes the error.
## Certbot's behavior differed from what I expected because: it threw out this error when the credential file is simply dns_cloudflare_api_token = (token)


## Here is a Certbot log showing the issue (if available):
[letsencrypt.log](https://github.com/certbot/certbot/files/9591153/letsencrypt.log)

###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.

## Here is the relevant nginx server block or Apache virtualhost for the domain I am configuring: N/A



Discussion:
I'm sorry, but I'm unable to reproduce this error on my Gentoo system. I'm running latest Certbot from `git` and the Python Cloudflare library 2.10.1 and `certbot-dns-cloudflare` plugin 1.30.0 [from my overlay](https://github.com/osirisinferi/certbot-dns-plugins-overlay/tree/tmp).

Your error log shows "both key and token defined", but I'm unable to actually do that in the CF credentials file: Certbot complains if I use a token *and* a key. And with either a token or a key, it just works? So I don't know why Cloudflare would complain about your token and for me a token with the versions you're using works just fine.
I don't have a key actually defined, my whole credential file is just dns_cloudflare_api_token = (token)
I found why its happening: with cloudflare 2.10.1 ONLY its failing when I also have the normal config file for cloudflare itself present, its reading the normal cloudflare config file that specifies the same token
> when I also have the normal config file for cloudflare itself present, its reading the normal cloudflare config file that specifies the same token

You mean `~/.cloudflare/cloudflare.cfg`?
Yes

On Sat, Sep 17, 2022, 14:22 osirisinferi ***@***.***> wrote:

> when I also have the normal config file for cloudflare itself present, its
> reading the normal cloudflare config file that specifies the same token
>
> You mean ~/.cloudflare/cloudflare.cfg?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/certbot/certbot/issues/9407#issuecomment-1250127553>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAACZXQOQRWDNJLQNNC3JOTV6YLAZANCNFSM6AAAAAAQO4KSBU>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

I can reproduce your error indeed with Cloudflare 2.10.1 (when putting a token **and** a key in `cloudflare.cfg`), but for some reason I cannot get it *working* when downgrading to 2.9.10. When I have a token *and* a key in `cloudflare.cfg`, I'm getting the error:

> Error determining zone_id: 6003 Invalid request headers. Please confirm that you have supplied valid Cloudflare API credentials. (Did you copy your entire API token/key? To use Cloudflare tokens, you'll need the python package cloudflare>=2.3.1. This certbot is running cloudflare 2.9.10)

That said, it looks like your issue comes from your `cloudflare.cfg` configuration file which is, I think, outside of the scope of Certbot. I'm not sure if Certbot can interact with that part of the Cloudflare library.

I believe this is not as much a problem Certbot can fix, but perhaps Certbot can relay the error better: now it looks like the error regarding "confused info - both key and token defined" comes from the remote Cloudflare API endpoint, but it *probably* is just an error thrown by the Cloudflare **library** itself. I don't know if Certbot can detect the difference, but now it's confusing I think.
I don't have a key defined anywhere, my cloudflare.cfg is simply 
```
[Cloudflare]
token = (token)
```




Resolving patch:
diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py
--- a/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py
+++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/__init__.py
@@ -39,7 +39,7 @@
 zones you need certificates for.
 
 Using Cloudflare Tokens also requires at least version 2.3.1 of the ``cloudflare``
-python module. If the version that automatically installed with this plugin is
+Python module. If the version that automatically installed with this plugin is
 older than that, and you can't upgrade it on your system, you'll have to stick to
 the Global key.
 
@@ -77,6 +77,18 @@
 including for renewal, and cannot be silenced except by addressing the issue
 (e.g., by using a command like ``chmod 600`` to restrict access to the file).
 
+.. note::
+    Please note that the ``cloudflare`` Python module used by the plugin has
+    additional methods of providing credentials to the module, e.g. environment
+    variables or the ``cloudflare.cfg`` configuration file. These methods are not
+    supported by Certbot. If any of those additional methods of providing
+    credentials is being used, they must provide the same credentials (i.e.,
+    email and API key *or* an API token) as the credentials file provided to
+    Certbot. If there is a discrepancy, the ``cloudflare`` Python module will
+    raise an error. Also note that the credentials provided to Certbot will take
+    precedence over any other method of providing credentials to the ``cloudflare``
+    Python module.
+
 
 Examples
 --------
diff --git a/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py b/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
--- a/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
+++ b/certbot-dns-cloudflare/certbot_dns_cloudflare/_internal/dns_cloudflare.py
@@ -82,8 +82,9 @@ def _get_cloudflare_client(self) -> "_CloudflareClient":
         if not self.credentials:  # pragma: no cover
             raise errors.Error("Plugin has not been prepared.")
         if self.credentials.conf('api-token'):
-            return _CloudflareClient(None, self.credentials.conf('api-token'))
-        return _CloudflareClient(self.credentials.conf('email'), self.credentials.conf('api-key'))
+            return _CloudflareClient(api_token = self.credentials.conf('api-token'))
+        return _CloudflareClient(email = self.credentials.conf('email'),
+                                 api_key = self.credentials.conf('api-key'))
 
 
 class _CloudflareClient:
@@ -91,8 +92,19 @@ class _CloudflareClient:
     Encapsulates all communication with the Cloudflare API.
     """
 
-    def __init__(self, email: Optional[str], api_key: str) -> None:
-        self.cf = CloudFlare.CloudFlare(email, api_key)
+    def __init__(self, email: Optional[str] = None, api_key: Optional[str] = None,
+                 api_token: Optional[str] = None) -> None:
+        if email:
+            # If an email was specified, we're using an email/key combination and not a token.
+            # We can't use named arguments in this case, as it would break compatibility with
+            # the Cloudflare library since version 2.10.1, as the `token` argument was used for
+            # tokens and keys alike and the `key` argument did not exist in earlier versions.
+            self.cf = CloudFlare.CloudFlare(email, api_key)
+        else:
+            # If no email was specified, we're using just a token. Let's use the named argument
+            # for simplicity, which is compatible with all (current) versions of the Cloudflare
+            # library.
+            self.cf = CloudFlare.CloudFlare(token=api_token)
 
     def add_txt_record(self, domain: str, record_name: str, record_content: str,
                        record_ttl: int) -> None:


Test patch: