├── Dockerfile ├── README.md ├── certbot-certonly.sh ├── certbot-renew.sh ├── certbot.cron ├── cli.ini ├── haproxy-refresh.sh ├── haproxy-restart.sh ├── start.sh └── supervisord.conf /Dockerfile: -------------------------------------------------------------------------------- 1 | # haproxy1.6.9 with certbot 2 | FROM debian:jessie 3 | 4 | RUN apt-get update && apt-get install -y libssl1.0.0 libpcre3 --no-install-recommends && rm -rf /var/lib/apt/lists/* 5 | 6 | # Setup HAProxy 7 | ENV HAPROXY_MAJOR 1.6 8 | ENV HAPROXY_VERSION 1.6.9 9 | RUN buildDeps='curl gcc libc6-dev libpcre3-dev libssl-dev make' \ 10 | && set -x \ 11 | && apt-get update && apt-get install -y $buildDeps --no-install-recommends && rm -rf /var/lib/apt/lists/* \ 12 | && curl -SL "http://www.haproxy.org/download/${HAPROXY_MAJOR}/src/haproxy-${HAPROXY_VERSION}.tar.gz" -o haproxy.tar.gz \ 13 | && mkdir -p /usr/src/haproxy \ 14 | && tar -xzf haproxy.tar.gz -C /usr/src/haproxy --strip-components=1 \ 15 | && rm haproxy.tar.gz \ 16 | && make -C /usr/src/haproxy \ 17 | TARGET=linux2628 \ 18 | USE_PCRE=1 PCREDIR= \ 19 | USE_OPENSSL=1 \ 20 | USE_ZLIB=1 \ 21 | all \ 22 | install-bin \ 23 | && mkdir -p /config \ 24 | && mkdir -p /usr/local/etc/haproxy \ 25 | && cp -R /usr/src/haproxy/examples/errorfiles /usr/local/etc/haproxy/errors \ 26 | && rm -rf /usr/src/haproxy \ 27 | && apt-get purge -y --auto-remove $buildDeps 28 | 29 | # Install Supervisor, cron, libnl-utils, net-tools, iptables 30 | RUN apt-get update && apt-get install -y supervisor cron libnl-utils net-tools iptables && \ 31 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 32 | 33 | # Setup Supervisor 34 | RUN mkdir -p /var/log/supervisor 35 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 36 | 37 | # Install Certbot 38 | RUN echo 'deb http://ftp.debian.org/debian jessie-backports main' > /etc/apt/sources.list.d/jessie-backports.list 39 | RUN apt-get update && apt-get install -y certbot -t jessie-backports && \ 40 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 41 | 42 | # Setup Certbot 43 | RUN mkdir -p /usr/local/etc/haproxy/certs.d 44 | RUN mkdir -p /usr/local/etc/letsencrypt 45 | COPY certbot.cron /etc/cron.d/certbot 46 | COPY cli.ini /usr/local/etc/letsencrypt/cli.ini 47 | COPY haproxy-refresh.sh /usr/bin/haproxy-refresh 48 | COPY haproxy-restart.sh /usr/bin/haproxy-restart 49 | COPY certbot-certonly.sh /usr/bin/certbot-certonly 50 | COPY certbot-renew.sh /usr/bin/certbot-renew 51 | RUN chmod +x /usr/bin/haproxy-refresh /usr/bin/haproxy-restart /usr/bin/certbot-certonly /usr/bin/certbot-renew 52 | 53 | # Add startup script 54 | COPY start.sh /start.sh 55 | RUN chmod +x /start.sh 56 | 57 | # Start 58 | CMD ["/start.sh"] 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAProxy with Certbot 2 | 3 | Docker Container with haproxy and certbot. Haproxy is setup to use a 0 downtime 4 | reload method that queses requests when the Haproxy service is bounced as new 5 | certificates are added or existing certificates refreshed. 6 | 7 | #### Usage 8 | 9 | First some terminology... HAProxy is a reverse proxy load balancer among other 10 | things. Let's Encrypt is a service that allows the creation and renewal of SSL 11 | certificates at no cost through an API and with automatic authentication. 12 | Certbot is a Linux CLI tool for interfacing with the Let's Encrypt API. 13 | 14 | Certbot contains it's own http/https server and handles the authorization process 15 | from Let's Encrypt. This container is setup using HAProxy 16 | to redirect the Let's Encrypt callbacks (authentication) to the certbot http 17 | server while all other requests are directed to the backend server(s). 18 | This configuration of HAProxy is also setup todo all the SSL termination so that 19 | your backend server(s) do not require a SSL configuration or certificates to be 20 | installed. 21 | 22 | In order to use this in your environment, you must point all your SSL enabled 23 | domains to the IP Address of this container. This means updating the A Records 24 | for these domains with your DNS Provider. This includes the website name and all 25 | alternate names (i.e. example.com and www.example.com). After this is setup, 26 | an inbound request for your website(s) is initially received by HA Proxy. If the 27 | request is part of the Let's Encrypt authentication process, it will redirect 28 | that traffic to the local instance of certbot which is running on internal 29 | container ports 8080 and 8443. Otherwise it will pass through the request to a 30 | backend server (or servers) as defined in the haproxy.cfg file. The details of 31 | HAProxy setup are out of the scope for this README, but some examples are 32 | included below to get you started. 33 | 34 | ## Setup and Create Container 35 | 36 | This will create the haproxy-certbot container. Note that only the inbound ports 37 | for 80 and 443 are exposed. 38 | 39 | ```bash 40 | docker run -d \ 41 | --restart=always \ 42 | --name haproxy-certbot \ 43 | --cap-add=NET_ADMIN \ 44 | -p 80:80 \ 45 | -p 443:443 \ 46 | -v /docker/haproxy/config:/config \ 47 | -v /docker/haproxy/letsencrypt:/etc/letsencrypt \ 48 | -v /docker/haproxy/certs.d:/usr/local/etc/haproxy/certs.d \ 49 | nmarus/haproxy-certbot 50 | ``` 51 | 52 | It is important to note the mapping of the 3 volumes in the above command. This 53 | ensures that all non-persistent variable data is not maintained in the container 54 | itself. 55 | 56 | The description of the 3 mapped volumes are as follows: 57 | 58 | * `/config` - The configuration file location for haproxy.cfg 59 | * `/etc/letsencrypt` - The directory that Let's Encrypt will store it's 60 | configuration, certificates and private keys. **It is of significant 61 | importance that you maintain a backup of this folder in the event the data is 62 | lost or corrupted.** 63 | * `/usr/local/etc/haproxy/certs.d` - The directory that this container will 64 | store the processed certs/keys from Let's Encrypt after they have been 65 | converted into a format that HAProxy can use. This is automatically done at 66 | each refresh and can also be manually initiated. This volume is not as 67 | important as the previous as the certs used by HAProxy can be regenerated 68 | again based on the contents of the letsencrypt folder. 69 | 70 | ## Container Helper Scripts 71 | 72 | There are a handful of helper scripts to ease the amount of configuration 73 | parameters needed to administer this container. 74 | 75 | #### Add a New Cert 76 | 77 | This will add a new cert using a certbot config that is compatible with the 78 | haproxy config template below. After creating the cert, you should run the 79 | refresh script referenced below to initialize haproxy to use it. After adding 80 | the cert and running the refresh script, no further action is needed. 81 | 82 | ***This example assumes you named you haproxy-certbot container using the same 83 | name as above when it was created. If not, adjust appropriately.*** 84 | 85 | ```bash 86 | # request certificate from let's encrypt 87 | docker exec haproxy-certbot certbot-certonly \ 88 | --domain example.com \ 89 | --domain www.example.com \ 90 | --email nmarus@gmail.com \ 91 | --dry-run 92 | 93 | # create/update haproxy formatted certs in certs.d and then restart haproxy 94 | docker exec haproxy-certbot haproxy-refresh 95 | ``` 96 | 97 | *After testing the setup, remove `--dry-run` to generate a live certificate* 98 | 99 | #### Renew a Cert 100 | 101 | Renewing happens automatically but should you choose to renew manually, you can 102 | do the following. 103 | 104 | ***This example assumes you named you haproxy-certbot container using the same 105 | name as above when it was created. If not, adjust appropriately.*** 106 | 107 | ```bash 108 | docker exec haproxy-certbot certbot-renew \ 109 | --dry-run 110 | ``` 111 | 112 | *After testing the setup, remove `--dry-run` to refresh a live certificate* 113 | 114 | #### Create/Refresh Certs used by HAProxy from Let's Encrypt 115 | 116 | This will parse and individually concatenate all the certs found in 117 | `/etc/letsencrypt/live` directory into the folder 118 | `/usr/local/etc/haproxy/certs.d`. It additionally will restart the HAProxy 119 | service so that the new certs are active. 120 | 121 | When HAProxy is restarted, the system will queue requests using tc and libnl and 122 | minimal to 0 interruption of the HAProxy services is expected. 123 | 124 | See [this blog entry](https://engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html) for more details. 125 | 126 | **Note: This process automatically happens whenever the cron job runs to refresh 127 | the certificates that have been registered.** 128 | 129 | ```bash 130 | docker exec haproxy-certbot haproxy-refresh 131 | ``` 132 | 133 | ### Example haproxy.cfg 134 | 135 | ##### Using Cluster Backend 136 | 137 | This example intercepts the Let's Encrypt validation and redirects to certbot. 138 | Normal traffic is passed to the backend servers. If the request arrives as a 139 | http request, it is redirected to https. If there is not a certificate installed 140 | for the requested website, haproxy will present a self signed default 141 | certificate. This behavior can be modified by adapting the haproxy config file 142 | if so desired. 143 | 144 | This example also does not do any routing based on the URL. It assumes that all 145 | domains pointed to this haproxy instance exist on the same backend server(s). 146 | The backend setup in this example consists of 3 web server that haproxy will 147 | load balance against. If there is only a single server, or a different quantity 148 | this can be adjusted in the backend configuration block. This specific example 149 | would be a configuration that could be used in front of a PaaS cluster such 150 | as Flynn.io or Tsuru.io (both of which have their own http router in order to 151 | direct the traffic to the required application). 152 | 153 | ``` 154 | global 155 | maxconn 1028 156 | 157 | log 127.0.0.1 local0 158 | log 127.0.0.1 local1 notice 159 | 160 | ca-base /etc/ssl/certs 161 | crt-base /etc/ssl/private 162 | 163 | ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS 164 | ssl-default-bind-options no-sslv3 165 | 166 | defaults 167 | option forwardfor 168 | 169 | log global 170 | 171 | timeout connect 5000ms 172 | timeout client 50000ms 173 | timeout server 50000ms 174 | 175 | stats enable 176 | stats uri /stats 177 | stats realm Haproxy\ Statistics 178 | stats auth admin:haproxy 179 | 180 | frontend http-in 181 | bind *:80 182 | mode http 183 | 184 | reqadd X-Forwarded-Proto:\ http 185 | 186 | acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ 187 | redirect scheme https if !letsencrypt_http_acl 188 | use_backend letsencrypt_http if letsencrypt_http_acl 189 | 190 | default_backend my_http_backend 191 | 192 | frontend https_in 193 | bind *:443 ssl crt /usr/local/etc/haproxy/default.pem crt /usr/local/etc/haproxy/certs.d ciphers ECDHE-RSA-AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM 194 | mode http 195 | 196 | reqadd X-Forwarded-Proto:\ https 197 | 198 | default_backend my_http_backend 199 | 200 | backend letsencrypt_http 201 | mode http 202 | server letsencrypt_http_srv 127.0.0.1:8080 203 | 204 | backend my_http_backend 205 | mode http 206 | balance leastconn 207 | option tcp-check 208 | option log-health-checks 209 | server server1 1.1.1.1:80 check port 80 210 | server server2 2.2.2.2:80 check port 80 211 | server server3 3.3.3.3:80 check port 80 212 | ``` 213 | -------------------------------------------------------------------------------- /certbot-certonly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/certbot certonly -c /usr/local/etc/letsencrypt/cli.ini "$@" 4 | -------------------------------------------------------------------------------- /certbot-renew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/certbot renew "$@" 4 | -------------------------------------------------------------------------------- /certbot.cron: -------------------------------------------------------------------------------- 1 | # /etc/cron.d/certbot: crontab entries for the certbot package 2 | # 3 | # Upstream recommends attempting renewal twice a day 4 | # 5 | # Eventually, this will be an opportunity to validate certificates 6 | # haven't been revoked, etc. Renewal will only occur if expiration 7 | # is within 30 days. 8 | SHELL=/bin/sh 9 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 10 | 11 | 0 */12 * * * root test -x /usr/bin/certbot && perl -e 'sleep int(rand(3600))' && certbot -q renew && haproxy-refresh 12 | -------------------------------------------------------------------------------- /cli.ini: -------------------------------------------------------------------------------- 1 | authenticator = standalone 2 | agree-tos = True 3 | http-01-port = 8080 4 | tls-sni-01-port = 8443 5 | non-interactive = True 6 | standalone-supported-challenges = http-01 7 | -------------------------------------------------------------------------------- /haproxy-refresh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | HA_PROXY_DIR=/usr/local/etc/haproxy 4 | LE_DIR=/etc/letsencrypt/live 5 | DOMAINS=$(ls ${LE_DIR}) 6 | 7 | # update certs for HA Proxy 8 | for DOMAIN in ${DOMAINS} 9 | do 10 | cat ${LE_DIR}/${DOMAIN}/fullchain.pem ${LE_DIR}/${DOMAIN}/privkey.pem > ${HA_PROXY_DIR}/certs.d/${DOMAIN}.pem 11 | done 12 | 13 | # restart haproxy 14 | exec /usr/bin/haproxy-restart 15 | -------------------------------------------------------------------------------- /haproxy-restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --buffer 4 | /usr/local/sbin/haproxy -f /config/haproxy.cfg -D -p /var/run/haproxy.pid -sf $(cat /var/run/haproxy.pid) 5 | nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --release-indefinite 6 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | HA_PROXY_DIR=/usr/local/etc/haproxy 6 | TEMP_DIR=/tmp 7 | 8 | PASSWORD=$(openssl rand -base64 32) 9 | SUBJ="/C=US/ST=somewhere/L=someplace/O=haproxy/OU=haproxy/CN=haproxy.selfsigned.invalid" 10 | 11 | KEY=${TEMP_DIR}/haproxy_key.pem 12 | CERT=${TEMP_DIR}/haproxy_cert.pem 13 | CSR=${TEMP_DIR}/haproxy.csr 14 | DEFAULT_PEM=${HA_PROXY_DIR}/default.pem 15 | CONFIG=/config/haproxy.cfg 16 | 17 | # Check if config file for haproxy exists 18 | if [ ! -e ${CONFIG} ]; then 19 | echo "${CONFIG} not found" 20 | exit 1 21 | fi 22 | 23 | # Check if default.pem has been created 24 | if [ ! -e ${DEFAULT_PEM} ]; then 25 | openssl genrsa -des3 -passout pass:${PASSWORD} -out ${KEY} 2048 &> /dev/null 26 | openssl req -new -key ${KEY} -passin pass:${PASSWORD} -out ${CSR} -subj ${SUBJ} &> /dev/null 27 | cp ${KEY} ${KEY}.org &> /dev/null 28 | openssl rsa -in ${KEY}.org -passin pass:${PASSWORD} -out ${KEY} &> /dev/null 29 | openssl x509 -req -days 3650 -in ${CSR} -signkey ${KEY} -out ${CERT} &> /dev/null 30 | cat ${CERT} ${KEY} > ${DEFAULT_PEM} 31 | echo ${PASSWORD} > /password.txt 32 | fi 33 | 34 | # Mark Syn Packets 35 | IP=$(echo `ifconfig eth0 2>/dev/null|awk '/inet addr:/ {print $2}'|sed 's/addr://'`) 36 | /sbin/iptables -t mangle -I OUTPUT -p tcp -s ${IP} --syn -j MARK --set-mark 1 37 | 38 | # Set up the queuing discipline 39 | tc qdisc add dev lo root handle 1: prio bands 4 40 | tc qdisc add dev lo parent 1:1 handle 10: pfifo limit 1000 41 | tc qdisc add dev lo parent 1:2 handle 20: pfifo limit 1000 42 | tc qdisc add dev lo parent 1:3 handle 30: pfifo limit 1000 43 | 44 | # Create a plug qdisc with 32 meg of buffer 45 | nl-qdisc-add --dev=lo --parent=1:4 --id=40: plug --limit 33554432 46 | # Release the plug 47 | nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --release-indefinite 48 | 49 | # Set up the filter, any packet marked with "1" will be 50 | # directed to the plug 51 | tc filter add dev lo protocol ip parent 1:0 prio 1 handle 1 fw classid 1:4 52 | 53 | # Run Supervisor 54 | exec /usr/bin/supervisord 55 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon = true 3 | 4 | [program:crond] 5 | command = /usr/sbin/cron -f 6 | 7 | [program:haproxy] 8 | autorestart = unexpected 9 | startsecs = 0 10 | command = bash -c "/usr/local/sbin/haproxy -f /config/haproxy.cfg -D -p /var/run/haproxy.pid -sf $(cat /var/run/haproxy.pid)" 11 | --------------------------------------------------------------------------------