Repository: certbot/certbot

Issue:
Apache plugin should work with restrictive umask
This problem was initially described at https://github.com/certbot/certbot/issues/1761#issuecomment-581678866.

##  I ran this command and it produced this output:
```
# umask 0077
# certbot -d example.org --apache
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator apache, Installer apache
Registering without email!
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for example.org
Enabled Apache rewrite module
Waiting for verification...
Challenge failed for domain example.org
http-01 challenge for example.org
Cleaning up challenges
Some challenges have failed.

IMPORTANT NOTES:
 - The following errors were reported by the server:

   Domain: example.org
   Type:   unauthorized
   Detail: Invalid response from
   http://example.org/.well-known/acme-challenge/dYsk27k0uIUgA1QOewleMrShVzv_BLZx7nP5H5eDfww
   [2604:a880:2:d0::578:6001]: "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD
   HTML 2.0//EN\">\n<html><head>\n<title>403
   Forbidden</title>\n</head><body>\n<h1>Forbidden</h1>\n<p"

   To fix these errors, please make sure that your domain name was 
   entered correctly and the DNS A/AAAA record(s) for that domain
   contain(s) the right IP address.
```

## Certbot's behavior differed from what I expected because:

The challenge failed.

## Discussion

The problem is we're not accounting for the possibility of a restrictive umask in the Apache plugin when setting up the challenge. We should fix this like we do in the webroot plugin.

~~I also think we should check if we have the same problem in the nginx plugin.~~


Discussion:

Resolving patch:
diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py
--- a/certbot-apache/certbot_apache/_internal/http_01.py
+++ b/certbot-apache/certbot_apache/_internal/http_01.py
@@ -1,5 +1,6 @@
 """A class that performs HTTP-01 challenges for Apache"""
 import logging
+import errno
 
 from acme.magic_typing import List  # pylint: disable=unused-import, no-name-in-module
 from acme.magic_typing import Set  # pylint: disable=unused-import, no-name-in-module
@@ -168,7 +169,15 @@ def _relevant_vhosts(self):
 
     def _set_up_challenges(self):
         if not os.path.isdir(self.challenge_dir):
-            filesystem.makedirs(self.challenge_dir, 0o755)
+            old_umask = os.umask(0o022)
+            try:
+                filesystem.makedirs(self.challenge_dir, 0o755)
+            except OSError as exception:
+                if exception.errno not in (errno.EEXIST, errno.EISDIR):
+                    raise errors.PluginError(
+                        "Couldn't create root for http-01 challenge")
+            finally:
+                os.umask(old_umask)
 
         responses = []
         for achall in self.achalls:


Test patch:
diff --git a/certbot-apache/tests/http_01_test.py b/certbot-apache/tests/http_01_test.py
--- a/certbot-apache/tests/http_01_test.py
+++ b/certbot-apache/tests/http_01_test.py
@@ -1,5 +1,6 @@
 """Test for certbot_apache._internal.http_01."""
 import unittest
+import errno
 
 import mock
 
@@ -197,6 +198,12 @@ def common_perform_test(self, achalls, vhosts):
 
         self.assertTrue(os.path.exists(challenge_dir))
 
+    @mock.patch("certbot_apache._internal.http_01.filesystem.makedirs")
+    def test_failed_makedirs(self, mock_makedirs):
+        mock_makedirs.side_effect = OSError(errno.EACCES, "msg")
+        self.http.add_chall(self.achalls[0])
+        self.assertRaises(errors.PluginError, self.http.perform)
+
     def _test_challenge_conf(self):
         with open(self.http.challenge_conf_pre) as f:
             pre_conf_contents = f.read()