Repository: certbot/certbot

Issue:
--dry-run does not work properly with pre-authenticated DNS
I'm running snap-installed certbot on Debian10, with manual configuration of Apache.

We are using pre-authenticated DNS for acme (with --eab-kid and --eab-hmac-key), which should replace the use of challenges for renewal. When trying to test the renewal with --dry-run though, it fails because the dns challenge fails:

################################################################################
 certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/domain1.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for <domainname1> and <domainname2>

Certbot failed to authenticate some domains (authenticator: manual). The Certificate Authority reported these problems:
  Domain: domain2
  Type:   dns
  Detail: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.domain2 - check that a DNS record exists for this domain

  Domain: domain1
  Type:   dns
  Detail: DNS problem: NXDOMAIN looking up TXT for _acme-challenge.domain1 - check that a DNS record exists for this domain

Hint: The Certificate Authority failed to verify the DNS TXT records created by the --manual-auth-hook. Ensure that this hook is functioning correctly and that it waits a sufficient duration of time for DNS propagation. Refer to "certbot --help manual" and the Certbot User Guide.

Failed to renew certificate domain1 with error: Some challenges have failed.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
All simulated renewals failed. The following certificates could not be renewed:
  /etc/letsencrypt/live/domain1/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.
################################################################################

...however, when I test by forcing a renewal, it works correctly:

################################################################################
certbot renew --force-renewal
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/domain1.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for domain1 and domain2

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all renewals succeeded:
  /etc/letsencrypt/live/domain1/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
################################################################################

This indicates to me that using --dry-run causes certbot to take a different execution path than normal, one that causes the --manual-auth-hook and dns01 verification to be run, whereas real execution (with pre-authenticated DNS) does not do that (I have verified this). This renders the --dry-run flag useless to test the renewal under this mode of operation.

The certificate has been obtained with the following command line (obviously appropriately anonymised):

certbot certonly \
  --manual \
  --non-interactive \
  --agree-tos \
  --email <email> \
  --preferred-challenges dns \
  --manual-auth-hook '/bin/true' \
  --server server_url \
  --eab-kid xxxxxxxxxxxxxxxx \
  --eab-hmac-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
  --domains domain1,domain2 \
  --cert-name domain1



Discussion:
For better or worse, the behavior of `--dry-run` is to set the `--server` to `https://acme-staging-v02.api.letsencrypt.org/directory`, unless otherwise overridden on the CLI or in `/etc/letsencrypt/cli.ini`.

If your ACME provider has a testing endpoint that provides the same preauthorization, then what you're looking for would be `--dry-run --server https://testing.your.acme.server/dir`.

If your ACME provider doesn't have a testing endpoint, then there's not really any way to do a dry-run. It would just be a live run which discards the certificate.

I don't think we currently plan to do anything about this edge case, since it only affects a very small number of our users. The CLI documentation could be clearer (https://eff-certbot.readthedocs.io/en/stable/using.html#changing-the-acme-server), which is the only action item I can think of here.
Fair enough. Perhaps the --help could say something like: 

``  --dry-run         Test "renew" or "certonly" without saving any certificates to disk (letsencrypt only or use --server) ``

...to clarify that this is a letsencrypt feature which may or may not be available with other providers. 


Resolving patch:
diff --git a/certbot/certbot/_internal/cli/__init__.py b/certbot/certbot/_internal/cli/__init__.py
--- a/certbot/certbot/_internal/cli/__init__.py
+++ b/certbot/certbot/_internal/cli/__init__.py
@@ -149,16 +149,13 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st
         [None, "testing", "renew", "certonly"],
         "--dry-run", action="store_true", dest="dry_run",
         default=flag_default("dry_run"),
-        help="Perform a test run of the client, obtaining test (invalid) certificates"
-             " but not saving them to disk. This can currently only be used"
-             " with the 'certonly' and 'renew' subcommands. \nNote: Although --dry-run"
-             " tries to avoid making any persistent changes on a system, it "
-             " is not completely side-effect free: if used with webserver authenticator plugins"
-             " like apache and nginx, it makes and then reverts temporary config changes"
-             " in order to obtain test certificates, and reloads webservers to deploy and then"
-             " roll back those changes.  It also calls --pre-hook and --post-hook commands"
-             " if they are defined because they may be necessary to accurately simulate"
-             " renewal. --deploy-hook commands are not called.")
+        help="Perform a test run against the Let's Encrypt staging server, obtaining test"
+             " (invalid) certificates but not saving them to disk. This can only be used with the"
+             " 'certonly' and 'renew' subcommands. It may trigger webserver reloads to "
+             " temporarily modify & roll back configuration files."
+             " --pre-hook and --post-hook commands run by default."
+             " --deploy-hook commands do not run, unless enabled by --run-deploy-hooks."
+             " The test server may be overridden with --server.")
     helpful.add(
         ["testing", "renew", "certonly", "reconfigure"],
         "--run-deploy-hooks", action="store_true", dest="run_deploy_hooks",
@@ -270,8 +267,8 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st
     # overwrites server, handled in HelpfulArgumentParser.parse_args()
     helpful.add(["testing", "revoke", "run"], "--test-cert", "--staging",
         dest="staging", action="store_true", default=flag_default("staging"),
-        help="Use the staging server to obtain or revoke test (invalid) certificates; equivalent"
-             " to --server " + constants.STAGING_URI)
+        help="Use the Let's Encrypt staging server to obtain or revoke test (invalid) "
+             "certificates; equivalent to --server " + constants.STAGING_URI)
     helpful.add(
         "testing", "--debug", action="store_true", default=flag_default("debug"),
         help="Show tracebacks in case of errors")


Test patch: