From f282688a47568c825ddf3c68fa3b6c62b7c48d5a Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Thu, 5 Nov 2015 20:29:29 +0100 Subject: [PATCH 1/1] Initial checkin. --- README | 36 ++++++++++++++++++ setup.py | 16 ++++++++ varnish.py | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 README create mode 100644 setup.py create mode 100644 varnish.py diff --git a/README b/README new file mode 100644 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 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 diff --git a/setup.py b/setup.py new file mode 100644 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 index 0000000..87abf22 --- /dev/null +++ b/varnish.py @@ -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"]) -- 2.39.2