Initial checkin.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 5 Nov 2015 19:29:29 +0000 (20:29 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Thu, 5 Nov 2015 19:29:29 +0000 (20:29 +0100)
README [new file with mode: 0644]
setup.py [new file with mode: 0644]
varnish.py [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..fb4d683
--- /dev/null
+++ b/README
@@ -0,0 +1,36 @@
+This is a Let's Encrypt authenticator module for authenticating any site
+that Varnish sits in front of. The most obvious use would be if you have
+some sort of backend CMS that's not easy to get to serve arbitrary files,
+or if you have some sort of complicated rewriting in place in your VCL.
+It works by rewriting your VCL file to intercept the http-01 auth requests
+and synthesizing the responses.
+
+The code is ugly, has tons of lint errors and relies on a number of assumptions
+(such as your VCL being in /etc/default/varnish.vcl). Patches accepted to clean
+it up. Please back up your VCL configuratoin before use.
+
+To use:
+
+ 1. Install letsencrypt as usual, with letsencrypt-auto.
+
+ 2. Activate the venv:
+
+    . ~/.local/share/letsencrypt/bin/activate
+
+ 3. Install the module:
+
+    pip install -e path/to/this/letsencrypt-varnish
+
+ 4. Ask for a certificate:
+
+    sudo ~/.local/share/letsencrypt/bin/letsencrypt --agree-dev-preview --server https://acme-v01.api.letsencrypt.org/directory -a letsencrypt-varnish-plugin:varnish -d <domain> certonly  
+
+
+There is no installation module, since Varnish itself does not support SSL.
+If there's enough interest, I might make a hitch installation module to go
+with the Varnish authenticator module.
+
+The Varnish authenticator plugin is licensed under the same terms as the Let's
+Encrypt client itself.
+
+  - Steinar H. Gunderson <steinar+letsencrypt@gunderson.no>
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..afe53f3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+from setuptools import setup
+
+
+setup(
+    name='letsencrypt-varnish-plugin',
+    package='varnish.py',
+    install_requires=[
+        'letsencrypt',
+        'zope.interface',
+    ],
+    entry_points={
+        'letsencrypt.plugins': [
+            'varnish = varnish:Authenticator',
+        ],
+    },
+)
diff --git a/varnish.py b/varnish.py
new file mode 100644 (file)
index 0000000..87abf22
--- /dev/null
@@ -0,0 +1,108 @@
+"""Plugin for authenticating directly out of Varnish's VCL."""
+import os
+import logging
+import re
+import subprocess
+
+import zope.component
+import zope.interface
+
+from acme import challenges
+
+from letsencrypt import errors
+from letsencrypt import interfaces
+from letsencrypt.plugins import common
+
+
+logger = logging.getLogger(__name__)
+
+def vcl_recv_line(achall):
+    # Don't bother checking for the right host, we could be coming in through a redirect.
+    return 'if (req.url == "/%s/%s") { return (synth(999, "Challenge")); }  # Added by letsencrypt Varnish plugin for authentication\n' % (achall.URI_ROOT_PATH, achall.chall.encode("token"))
+
+def vcl_synth_line(validation):
+    return 'if (resp.status == 999) { set resp.status = 200; set resp.http.Content-Type = "text/plain"; synthetic("%s"); return (deliver); }  # Added by letsencrypt Varnish plugin for authentication\n' % (validation);
+
+class Authenticator(common.Plugin):
+    zope.interface.implements(interfaces.IAuthenticator)
+    zope.interface.classProvides(interfaces.IPluginFactory)
+    hidden = True
+
+    description = "Manual configuration, authentication via Varnish VCL"
+
+    def __init__(self, *args, **kwargs):
+        super(Authenticator, self).__init__(*args, **kwargs)
+        self._httpd = None
+
+    def prepare(self):  # pylint: disable=missing-docstring,no-self-use
+        pass  # pragma: no cover
+
+    def more_info(self):  # pylint: disable=missing-docstring,no-self-use
+        return ("")
+
+    def get_chall_pref(self, domain):
+        # pylint: disable=missing-docstring,no-self-use,unused-argument
+        return [challenges.HTTP01]
+
+    def perform(self, achalls):  # pylint: disable=missing-docstring
+        responses = []
+        for achall in achalls:
+            responses.append(self._perform_single(achall))
+        return responses
+
+    def _perform_single(self, achall):
+        # same path for each challenge response would be easier for
+        # users, but will not work if multiple domains point at the
+        # same server: default command doesn't support virtual hosts
+        response, validation = achall.response_and_validation()
+
+        with open("/etc/varnish/default.vcl") as vcl:
+            content = vcl.readlines()
+
+        self.old_content = content
+
+        found_vcl_recv = False
+        found_vcl_synth = False
+        new_content = []
+        for line in content:
+            if re.search("# Added by letsencrypt Varnish plugin", line):
+                # Don't include this line; left by a previous run.
+                continue
+            new_content.append(line)
+            if re.search("^sub\s+vcl_recv\s+{\s*$", line):
+                new_content.append(vcl_recv_line(achall))
+                found_vcl_recv = True
+            if re.search("^sub\s+vcl_synth\s+{\s*$", line):
+                new_content.append(vcl_synth_line(validation))
+                found_vcl_synth = True
+
+        if not found_vcl_recv:
+            new_content.append("sub vcl_recv {  # Added by letsencrypt Varnish plugin for authentication\n")
+            new_content.append(vcl_recv_line(achall))
+            new_content.append("}  # Added by letsencrypt Varnish plugin for authentication\n")
+
+        if not found_vcl_synth:
+            new_content.append("sub vcl_synth {  # Added by letsencrypt Varnish plugin for authentication\n")
+            new_content.append(vcl_synth_line(validation))
+            new_content.append("}  # Added by letsencrypt Varnish plugin for authentication\n")
+
+        with open("/etc/varnish/default.vcl", "w") as vcl:
+            vcl.writelines(new_content)
+
+        subprocess.call(["systemctl", "reload", "varnish.service"])
+
+        if response.simple_verify(
+                achall.chall, achall.domain,
+                achall.account_key.public_key(), self.config.http01_port):
+            return response
+        else:
+            logger.error(
+                "Self-verify of challenge failed, authorization abandoned!")
+            return None
+
+    def cleanup(self, achalls):
+        # pylint: disable=missing-docstring,no-self-use,unused-argument
+        with open("/etc/varnish/default.vcl", "w") as vcl:
+            vcl.writelines(self.old_content)
+
+        subprocess.call(["systemctl", "reload", "varnish.service"])