Repository: certbot/certbot

Issue:
Add a reminder about IP address privacy to the manual mode client
People may decide to run, e.g. the manual authenticator from their laptop rather than from the server. This will work fine. However, the Let's Encrypt privacy policy states that we will retain and publish IP addresses associated with ACME validation requests, including requestor IP. This is unremarkable when running the client on the same machine where a cert will be deployed. When running the client on a personal machine, this behavior will be unexpected to anyone who hasn't read the privacy policy.

We should do a check that the resolved names being authorized point at the current machine. If they don't we should provide a notice to the user that their IP address will be published, along with a link to the privacy policy.



Discussion:
It's extremely difficult to determine if you are on the machine that DNS points to for a given name.  You have to learn your public IP.  You might be on Amazon, where you think you have a private IP, but some public IP actually routes to you.

Instead, Brad suggested a simpler approach of having the manual installer say something like "NOTE: The IP of this machine will be publicly logged as having requested this certificate. If you're running letsencrypt in manual mode on your laptop, please ensure you're okay with that.."

Agreed it's very difficult. However, we probably want to try in a few other places anyhow. For instance, when validating that we've correctly provisioned a challenge.

To expand on that a bit more: to validate a challenge, we probably want to look up the DNS for the name being validated and connect to the first IP we see. If we fail, we should give an informative message.

Similarly, if we want to check whether someone's likely on a different machine than their server, we can start up a little server at startup and try to connect to it by the IP we look up.

Although, now that I say it, I realize that that's not an option in manual mode.

At any rate, having a hardcoded warning on manual mode is fine for now, I think. :-)


Resolving patch:
diff --git a/letsencrypt/plugins/manual.py b/letsencrypt/plugins/manual.py
--- a/letsencrypt/plugins/manual.py
+++ b/letsencrypt/plugins/manual.py
@@ -46,6 +46,15 @@ class Authenticator(common.Plugin):
 command on the target server (as root):
 
 {command}
+"""
+
+    # a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
+    IP_DISCLAIMER = """\
+NOTE: The IP of this machine will be publicly logged as having requested this certificate. \
+If you're running letsencrypt in manual mode on a machine that is not your server, \
+please ensure you're okay with that.
+
+Are you OK with your IP being logged?
 """
 
     # "cd /tmp/letsencrypt" makes sure user doesn't serve /root,
@@ -151,6 +160,10 @@ def _perform_single(self, achall):
             if self._httpd.poll() is not None:
                 raise errors.Error("Couldn't execute manual command")
         else:
+            if not zope.component.getUtility(interfaces.IDisplay).yesno(
+                    self.IP_DISCLAIMER, "Yes", "No"):
+                raise errors.PluginError("Must agree to IP logging to proceed")
+
             self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
                 validation=validation.json_dumps(), response=response,
                 uri=response.uri(achall.domain, achall.challb.chall),


Test patch:
diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py
--- a/letsencrypt/plugins/manual_test.py
+++ b/letsencrypt/plugins/manual_test.py
@@ -43,11 +43,13 @@ def test_get_chall_pref(self):
     def test_perform_empty(self):
         self.assertEqual([], self.auth.perform([]))
 
+    @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility")
     @mock.patch("letsencrypt.plugins.manual.sys.stdout")
     @mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify")
     @mock.patch("__builtin__.raw_input")
-    def test_perform(self, mock_raw_input, mock_verify, mock_stdout):
+    def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
         mock_verify.return_value = True
+        mock_interaction().yesno.return_value = True
 
         resp = challenges.SimpleHTTPResponse(tls=False)
         self.assertEqual([resp], self.auth.perform(self.achalls))
@@ -61,6 +63,15 @@ def test_perform(self, mock_raw_input, mock_verify, mock_stdout):
         mock_verify.return_value = False
         self.assertEqual([None], self.auth.perform(self.achalls))
 
+    @mock.patch("letsencrypt.plugins.manual.zope.component.getUtility")
+    @mock.patch("letsencrypt.plugins.manual.Authenticator._notify_and_wait")
+    def test_disagree_with_ip_logging(self, mock_notify, mock_interaction):
+        mock_interaction().yesno.return_value = False
+        mock_notify.side_effect = errors.Error("Exception not raised, \
+            continued execution even after disagreeing with IP logging")
+
+        self.assertRaises(errors.PluginError, self.auth.perform, self.achalls)
+
     @mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
     def test_perform_test_command_oserror(self, mock_popen):
         mock_popen.side_effect = OSError