Repository: certbot/certbot

Issue:
Apache authenticator crashes on a completely empty virtualhost
This was reported at https://community.letsencrypt.org/t/cant-create-cert-for-a-domain-error-augeas-insert/155400.

In 1.17.0, https://github.com/certbot/certbot/pull/8898 was merged, to authenticate all virtual hosts which do not have an explicit `ServerName`.

As a result, Certbot tries to authenticate more vhosts than before.

Unfortunately, this has exposed a bug with the authenticator that causes a crash if there are not any directives in the candidate vhost.

This will crash:

```apache
<VirtualHost *:80>
</VirtualHost>
```

so will this (because apparently as the vhost is represented in Augeas, `/directive[1]` is empty):

```apache
<VirtualHost *:80>
  <Location />
        Require all denied
  </Location>
</VirtualHost>
```

The stack trace is:

    2021-07-14 08:55:47,165:DEBUG:certbot_apache._internal.http_01:Adding a temporary challenge validation Include for name: None in: /etc/apache2/sites-enabled/000-default.conf
    2021-07-14 08:55:47,166:DEBUG:certbot._internal.error_handler:Encountered exception:
    Traceback (most recent call last):
      File "/home/alex/devel/certbot/certbot/certbot/_internal/auth_handler.py", line 73, in handle_authorizations
        resps = self.auth.perform(achalls)
      File "/home/alex/devel/certbot/certbot-apache/certbot_apache/_internal/configurator.py", line 2538, in perform
        http_response = http_doer.perform()
      File "/home/alex/devel/certbot/certbot-apache/certbot_apache/_internal/http_01.py", line 76, in perform
        self._mod_config()
      File "/home/alex/devel/certbot/certbot-apache/certbot_apache/_internal/http_01.py", line 120, in _mod_config
        self._set_up_include_directives(vh)
      File "/home/alex/devel/certbot/certbot-apache/certbot_apache/_internal/http_01.py", line 216, in _set_up_include_directives
        self.configurator.parser.add_dir_beginning(
      File "/home/alex/devel/certbot/certbot-apache/certbot_apache/_internal/parser.py", line 445, in add_dir_beginning
        self.aug.insert(first_dir, "directive", True)
      File "/home/alex/devel/certbot/venv/lib/python3.8/site-packages/augeas.py", line 369, in insert
        raise ValueError("Unable to insert label!")
    ValueError: Unable to insert label!

This needs to be fixed for 1.18.0 (or earlier). Self-note: don't forget to add these kinds of vhosts to the Apache test suites.

For readers, a workaround is to add a directive, like `ServerName example.com`, to the vhost (outside of the `<Location>` block).


Discussion:

Resolving patch:
diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py
--- a/certbot-apache/certbot_apache/_internal/parser.py
+++ b/certbot-apache/certbot_apache/_internal/parser.py
@@ -440,7 +440,11 @@ def add_dir_beginning(self, aug_conf_path, dirname, args):
         :type args: list or str
         """
         first_dir = aug_conf_path + "/directive[1]"
-        self.aug.insert(first_dir, "directive", True)
+        if self.aug.get(first_dir):
+            self.aug.insert(first_dir, "directive", True)
+        else:
+            self.aug.set(first_dir, "directive")
+
         self.aug.set(first_dir, dirname)
         if isinstance(args, list):
             for i, value in enumerate(args, 1):


Test patch:
diff --git a/certbot-apache/tests/parser_test.py b/certbot-apache/tests/parser_test.py
--- a/certbot-apache/tests/parser_test.py
+++ b/certbot-apache/tests/parser_test.py
@@ -105,6 +105,11 @@ def test_add_dir_beginning(self):
         for i, match in enumerate(matches):
             self.assertEqual(self.parser.aug.get(match), str(i + 1))
 
+        for name in ("empty.conf", "no-directives.conf"):
+            conf = "/files" + os.path.join(self.parser.root, "sites-available", name)
+            self.parser.add_dir_beginning(conf, "AddDirectiveBeginning", "testBegin")
+            self.assertTrue(self.parser.find_dir("AddDirectiveBeginning", "testBegin", conf))
+
     def test_empty_arg(self):
         self.assertEqual(None,
                           self.parser.get_arg("/files/whatever/nonexistent"))
diff --git a/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf
new file mode 100644
diff --git a/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf
new file mode 100644
--- /dev/null
+++ b/certbot-apache/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf
@@ -0,0 +1,5 @@
+<VirtualHost *:80>
+  <Location />
+        Require all denied
+  </Location>
+</VirtualHost>