--- /dev/null
+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>
--- /dev/null
+"""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"])