Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom committed Mar 5, 2019
1 parent 52636cd commit 7b87b69
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 339 deletions.
48 changes: 48 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# start from debian 9 slim version
FROM debian:stretch-slim

# enable backport repo
RUN echo deb http://httpredir.debian.org/debian stretch-backports main | tee /etc/apt/sources.list.d/backports.list

RUN apt-get update && apt-get install --no-install-recommends -yqq \
gnupg \
apt-transport-https \
cron \
wget \
ca-certificates \
curl \
&& apt-get install --no-install-recommends -yqq certbot -t stretch-backports \
&& apt-get install --no-install-recommends -yqq supervisor \
&& apt-get clean autoclean && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# install haproxy 1.9 from official debian repos (https://haproxy.debian.net/)

RUN curl https://haproxy.debian.net/bernat.debian.org.gpg | apt-key add -
RUN echo deb http://haproxy.debian.net stretch-backports-1.9 main | tee /etc/apt/sources.list.d/haproxy.list

RUN apt-get update \
&& apt-get install -yqq haproxy=1.9.\* -t stretch-backports \
&& apt-get clean autoclean && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# See https://github.com/janeczku/haproxy-acme-validation-plugin
COPY haproxy-acme-validation-plugin/acme-http01-webroot.lua /etc/haproxy
COPY haproxy-acme-validation-plugin/cert-renewal-haproxy.sh /

COPY crontab.txt /var/crontab.txt
RUN crontab /var/crontab.txt && chmod 600 /etc/crontab

COPY supervisord.conf /etc/supervisord.conf
COPY certs.sh /
COPY bootstrap.sh /

RUN mkdir /jail

EXPOSE 80 443

VOLUME /etc/letsencrypt

COPY haproxy.cfg /etc/haproxy/haproxy.cfg

ENTRYPOINT ["/bootstrap.sh"]
339 changes: 0 additions & 339 deletions LICENSE

This file was deleted.

25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Dockerized HAProxy with Let's Encrypt automatic certificate renewal capabilities

This container provides a HAProxy 1.9 application with Let's Encrypt certificates
generated at startup, as well as renewed (if necessary) once a week.

## Usage

```
docker run \
-e CERTS=my.domain,my.other.domain \
-e EMAIL=my.email@my.domain \
-v /etc/letsencrypt:/etc/letsencrypt \
-p 80:80 -p 443:443 \
tomdess/docker-haproxy-letsencrypt
```


#### Customizing Haproxy

You will almost certainly want to create an image `FROM` this image or
mount your `haproxy.cfg` at `/etc/haproxy/haproxy.cfg`.


docker run [...] -v <override-conf-file>:/etc/haproxy/haproxy.cfg tomdess/docker-haproxy-letsencrypt

3 changes: 3 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

/certs.sh && supervisord -n
17 changes: 17 additions & 0 deletions certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash

if [ -n "$CERTS" ]; then
certbot certonly --no-self-upgrade -n --text --standalone \
--preferred-challenges http-01 \
-d "$CERTS" --keep --expand --agree-tos --email "$EMAIL" \
|| exit 1

mkdir -p /etc/haproxy/certs
for site in `ls -1 /etc/letsencrypt/live`; do
cat /etc/letsencrypt/live/$site/privkey.pem \
/etc/letsencrypt/live/$site/fullchain.pem \
| tee /etc/haproxy/certs/haproxy-"$site".pem >/dev/null
done
fi

exit 0
4 changes: 4 additions & 0 deletions crontab.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MAILTO=""
SHELL=/bin/bash

5 8 * * 6 /cert-renewal-haproxy.sh
21 changes: 21 additions & 0 deletions haproxy-acme-validation-plugin/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (C) 2015 Jan Broer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
107 changes: 107 additions & 0 deletions haproxy-acme-validation-plugin/acme-http01-webroot.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
-- ACME http-01 domain validation plugin for Haproxy 1.6+
-- copyright (C) 2015 Jan Broer
--
-- usage:
--
-- 1) copy acme-webroot.lua in your haproxy config dir
--
-- 2) Invoke the plugin by adding in the 'global' section of haproxy.cfg:
--
-- lua-load /etc/haproxy/acme-webroot.lua
--
-- 3) insert these two lines in every http frontend that is
-- serving domains for which you want to create certificates:
--
-- acl url_acme_http01 path_beg /.well-known/acme-challenge/
-- http-request use-service lua.acme-http01 if METH_GET url_acme_http01
--
-- 4) reload haproxy
--
-- 5) create a certificate:
--
-- ./letsencrypt-auto certonly --text --webroot --webroot-path /var/tmp -d blah.example.com --renew-by-default --agree-tos --email my@email.com
--

acme = {}
acme.version = "0.1.1"

--
-- Configuration
--
-- When HAProxy is *not* configured with the 'chroot' option you must set an absolute path here and pass
-- that as 'webroot-path' to the letsencrypt client

acme.conf = {
["non_chroot_webroot"] = "/jail"
}

--
-- Startup
--
acme.startup = function()
core.Info("[acme] http-01 plugin v" .. acme.version);
end

--
-- ACME http-01 validation endpoint
--
acme.http01 = function(applet)
local response = ""
local reqPath = applet.path
local src = applet.sf:src()
local token = reqPath:match( ".+/(.*)$" )

if token then
token = sanitizeToken(token)
end

if (token == nil or token == '') then
response = "bad request\n"
applet:set_status(400)
core.Warning("[acme] malformed request (client-ip: " .. tostring(src) .. ")")
else
auth = getKeyAuth(token)
if (auth:len() >= 1) then
response = auth .. "\n"
applet:set_status(200)
core.Info("[acme] served http-01 token: " .. token .. " (client-ip: " .. tostring(src) .. ")")
else
response = "resource not found\n"
applet:set_status(404)
core.Warning("[acme] http-01 token not found: " .. token .. " (client-ip: " .. tostring(src) .. ")")
end
end

applet:add_header("Server", "haproxy/acme-http01-authenticator")
applet:add_header("Content-Length", string.len(response))
applet:add_header("Content-Type", "text/plain")
applet:start_response()
applet:send(response)
end

--
-- strip chars that are not in the URL-safe Base64 alphabet
-- see https://github.com/letsencrypt/acme-spec/blob/master/draft-barnes-acme.md
--
function sanitizeToken(token)
_strip="[^%a%d%+%-%_=]"
token = token:gsub(_strip,'')
return token
end

--
-- get key auth from token file
--
function getKeyAuth(token)
local keyAuth = ""
local path = acme.conf.non_chroot_webroot .. "/.well-known/acme-challenge/" .. token
local f = io.open(path, "rb")
if f ~= nil then
keyAuth = f:read("*all")
f:close()
end
return keyAuth
end

core.register_init(acme.startup)
core.register_service("acme-http01", "http", acme.http01)
107 changes: 107 additions & 0 deletions haproxy-acme-validation-plugin/cert-renewal-haproxy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/bin/bash

# automation of certificate renewal for let's encrypt and haproxy
# - checks all certificates under /etc/letsencrypt/live and renews
# those about about to expire in less than 4 weeks
# - creates haproxy.pem files in /etc/letsencrypt/live/domain.tld/
# - soft-restarts haproxy to apply new certificates
# usage:
# sudo ./cert-renewal-haproxy.sh

###################
## configuration ##
###################

LE_CLIENT="certbot"

HAPROXY_RELOAD_CMD="supervisorctl signal HUP haproxy"

WEBROOT="/jail"

# Enable to redirect output to logfile (for silent cron jobs)
# LOGFILE="/var/log/certrenewal.log"

######################
## utility function ##
######################

function issueCert {
$LE_CLIENT certonly --text --webroot --webroot-path ${WEBROOT} --renew-by-default --agree-tos --email ${EMAIL} $1 &>/dev/null
return $?
}

function logger_error {
if [ -n "${LOGFILE}" ]
then
echo "[error] ${1}\n" >> ${LOGFILE}
fi
>&2 echo "[error] ${1}"
}

function logger_info {
if [ -n "${LOGFILE}" ]
then
echo "[info] ${1}\n" >> ${LOGFILE}
else
echo "[info] ${1}"
fi
}

##################
## main routine ##
##################

le_cert_root="/etc/letsencrypt/live"

if [ ! -d ${le_cert_root} ]; then
logger_error "${le_cert_root} does not exist!"
exit 1
fi

# check certificate expiration and run certificate issue requests
# for those that expire in under 4 weeks
renewed_certs=()
exitcode=0
while IFS= read -r -d '' cert; do
if ! openssl x509 -noout -checkend $((4*7*86400)) -in "${cert}"; then
subject="$(openssl x509 -noout -subject -in "${cert}" | grep -o -E 'CN=[^ ,]+' | tr -d 'CN=')"
subjectaltnames="$(openssl x509 -noout -text -in "${cert}" | sed -n '/X509v3 Subject Alternative Name/{n;p}' | sed 's/\s//g' | tr -d 'DNS:' | sed 's/,/ /g')"
domains="-d ${subject}"
for name in ${subjectaltnames}; do
if [ "${name}" != "${subject}" ]; then
domains="${domains} -d ${name}"
fi
done
issueCert "${domains}"
if [ $? -ne 0 ]
then
logger_error "failed to renew certificate! check /var/log/letsencrypt/letsencrypt.log!"
exitcode=1
else
renewed_certs+=("$subject")
logger_info "renewed certificate for ${subject}"
fi
else
logger_info "none of the certificates requires renewal"
fi
done < <(find ${le_cert_root} -name cert.pem -print0)

# create haproxy.pem file(s)
for domain in ${renewed_certs[@]}; do
cat ${le_cert_root}/${domain}/privkey.pem ${le_cert_root}/${domain}/fullchain.pem | tee /usr/local/etc/haproxy/certs/haproxy-${domain}.pem >/dev/null
if [ $? -ne 0 ]; then
logger_error "failed to create haproxy.pem file!"
exit 1
fi
done

# soft-restart haproxy
if [ "${#renewed_certs[@]}" -gt 0 ]; then
$HAPROXY_RELOAD_CMD
if [ $? -ne 0 ]; then
logger_error "failed to reload haproxy!"
exit 1
fi
fi

exit ${exitcode}
38 changes: 38 additions & 0 deletions haproxy.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
global
maxconn 256
lua-load /etc/haproxy/acme-http01-webroot.lua
chroot /jail
ssl-default-bind-ciphers AES256+EECDH:AES256+EDH:!aNULL;
tune.ssl.default-dh-param 4096

resolvers docker
nameserver dns "127.0.0.11:53"

defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option forwardfor
option http-server-close

# never fail on address resolution
default-server init-addr last,libc,none

frontend http
bind *:80
mode http
acl url_acme_http01 path_beg /.well-known/acme-challenge/
http-request use-service lua.acme-http01 if METH_GET url_acme_http01
redirect scheme https code 301 if !{ ssl_fc }

frontend https
bind *:443 ssl crt /etc/haproxy/certs/ no-sslv3 no-tls-tickets no-tlsv10 no-tlsv11

rspadd Strict-Transport-Security:\ max-age=15768000

default_backend www

backend www
server www www:80 check resolvers docker resolve-prefer ipv4
http-request add-header X-Forwarded-Proto https if { ssl_fc }
Loading

0 comments on commit 7b87b69

Please sign in to comment.