/ #haproxy #docker 

HAProxy LetsEncrypt Docker Tutorial: Create and Renew HTTPs Certificates for free

If you are looking to secure your website/API for free and found yourself on this page, that means you are on the good path :) Securing pages are required for security reasons, but also improve your ranking on search engines and can impact your social visibility.

This tutorial will show how to secure a golang API using HAProxy and letsencrypt.

What’s LetsEncrypt

LetsEncrypt is a free certificate authority launched on 2016. It automates the delivery of certificates used to secure the traffic. The certificate is valid for 90 days. The protocol ACME (Automated Certificate Management Environment) is used by LetsEncrypt to proof that you are the domain owner, to generate the certificate and to renew it.

My Simple Application

For demo, I will create a simple golang hello world API REST running on the port 5000. You can create your own server with your favorite language.

Run the app using the command go run main.go.

package main

import(
	"fmt"
	"net/http"
)

func main() {
    http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request){
      fmt.Fprintf(w, "Hello World")
    })
    http.ListenAndServe(":5000", nil)
}

Requirement

For this tutorial, you need a linux machine with an external IP address/FDQN. I am using Linode server because it is very performant and affordable.

You can click on this link or on the image below to get 100$ free credit.

HAProxy configuration

Now that we have a running application, let’s define the configuration of HAProxy that redirect the traffic to the application. Also, HAproxy should handle HTTPs requests and redirect all HTTP traffic to HTTPS.

myproject
|--haproxy
    |-- haproxy.cfg

On your root project folder, create a folder called haproxy. Add the file haproxy.cfg to the folder haproxy.

global
  log stdout format raw local0
  daemon

  # Default ciphers to use on SSL-enabled listening sockets.
  # For more information, see ciphers(1SSL).
  ssl-default-bind-ciphers kEECDH+aRSA+AES:kRSA+AES:+AES256:RC4-SHA:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL

resolvers docker_resolver
    nameserver dns 127.0.0.11:53

defaults
  log     global
  mode    http
  option  httplog
  option  dontlognull

frontend http
    bind *:80
    mode http
	
	# if this is an ACME request to proof the domain ownder, then redirect to nginx-certbot server
    acl is_well_known path_beg -i /.well-known/
	
	# else redirect the traffic to https
    redirect scheme https code 301 if !is_well_known !{ ssl_fc }
    use_backend letsencrypt if is_well_known

backend letsencrypt
    server letsencrypt nginx-certbot:80 resolvers docker_resolver check init-addr none

frontend https
    bind *:443 ssl crt /usr/local/etc/certs/
    http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"
    default_backend mybackend

backend mybackend
    server backend1 172.17.0.1:5000 
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

The above configuration will:

  • Redirects HTTP to HTTPS then redirect to our application by default
  • Handle letsencrypt HTTP requests for verifying the domain owner. These requests starts by /.well-known/ always and target a specific file that should be served in HTTP. That’s why we will use nginx to serve the target folder.
  • HTTPS requests will be secured using the certificates in /usr/local/etc/certs/. At least one certificate should be present. This is why it is important to create a dummy certificate before running haproxy. Otherwise, if the folder /usr/local/etc/certs/ is empty, the haproxy will show errors in log.

Create a dummy certificate

Create the folder certs at the root of your project.

myproject
|--certs
...

Create a self signed certificate using openssl.

$ sudo apt-get install openssl 
$ openssl req -nodes -x509 -newkey rsa:2048 -keyout test.key -out test.crt -days 30
$ cat test.key test.crt > ./certs/test.pem

If you like this tutorial, please give me support by subscribing to my Youtube channel my youtube channel

Run HAproxy

Create the folder webroot at the root of your project. This is the folder where Letsencrypt will request the file to verify that you are the owner of the domain.

myproject
|-- certs
|-- haproxy
|-- webroot
|-- docker-compose.yml
...

Now that the configurations and all the necessary folders are ready. Create the docker-compose.yml file on the root project directory.

version: "3.5"

services:
  proxy:
    image: haproxy:latest
    restart: always
    volumes:
      - ./haproxy:/usr/local/etc/haproxy:ro
      - ./certs:/usr/local/etc/certs:ro
    ports:
      - 80:80
      - 443:443

  nginx-certbot:
    image: nginx
    restart: always
    container_name: nginx-certbot
    volumes:
      - ./webroot:/usr/share/nginx/html

Run the haproxy and the nginx.

$ docker-compose up -d

If you are enjoying this tutorial, feel free to leave a comment. It means me a lot to know that you are appreciating my work :)

Create the certificate using certbot

Certbot is the letsencrypt official tool for creating a signed certificate. A certificate is valid for 90 days only and should be renewed always. By default, a production certificate is delivered. Therefore, don’t forget to use the option --staging for tests because Letsencrypt has rate limits.

Attention: Letsencrypt it can generates only 5 certificates per week. Take a look at their documentation

Create the script create-cert.sh at your root project:

#!/bin/bash

set -e

echo "Starting create new certificate..."
if [ "$#" -lt 2 ]; then
    echo "Usage: ...  <domain> <email> [options]"
    exit
fi

DOMAIN=$1
EMAIL=$2
OPTIONS=$3

docker run --rm \
  -v $PWD/letsencrypt:/etc/letsencrypt \
  -v $PWD/webroot:/webroot \
  certbot/certbot \
  certonly --webroot -w /webroot \
  -d $DOMAIN \
  --email $EMAIL \
  --non-interactive \
  --agree-tos \
  $3

# Merge private key and full chain in one file and add them to haproxy certs folder
function cat-cert() {
  dir="./letsencrypt/live/$1"
  cat "$dir/privkey.pem" "$dir/fullchain.pem" > "./certs/$1.pem"
}

# Run merge certificate for the requested domain name
cat-cert $DOMAIN

Explanation :

  • This script will take 3 arguments : domain name, email and options.
  • Webroot option : tell the certbot to create the file for domain verification in the path /webroot
  • Options : this is very important to add any additional option for certbot, example: --staging.

Run the script. Use –staging for test purposes. If your staging certificate is working, you can remove this option and create a production certificate later.

./create-cert yourdomain.com youremail.com  --staging

If you have created successfully your certificate, don’t forget to remove the dummy test certificate created certs/test.pem.

How it works:

  • The certbot command will create a verification file in webroot folder.
  • The letsencrypt service should be sent a request to the following url : http://yourdomainname.com/.well-known/random_verification_file_name
  • The request will arrive to haproxy. It will redirect the request to nginx-certbot which serves the webroot folder.
  • If the verification file is found, the certificate will should created in certs folder

Renew certificate

Letsencrypt certificates are valid only for 90 days. Therefore, it should be renewed always. I have done a script to renews all my certificates each month. Create this script at the project root folder and call it renew-certs.sh:

#!/bin/bash

set -e

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $DIR

echo "$(date) About to renew certificates" >> /var/log/letsencrypt-renew.log
/usr/bin/docker run \
       -i \
       --rm \
       --name certbot \
       -v $PWD/letsencrypt:/etc/letsencrypt \
       -v $PWD/webroot:/webroot \
       certbot/certbot \
       renew -w /webroot

echo "$(date) Cat certificates" >> /var/log/letsencrypt-renew.log

function cat-cert() {
  dir="./letsencrypt/live/$1"
  cat "$dir/privkey.pem" "$dir/fullchain.pem" > "./certs/$1.pem"
}

for dir in ./letsencrypt/live/*; do
  if [[ "$dir" != *"README" ]]; then
    cat-cert $(basename "$dir")
  fi
done

echo "$(date) Reload haproxy" >> /var/log/letsencrypt-renew.log
docker service update --force proxy_proxy

echo "$(date) Done" >> /var/log/letsencrypt-renew.log

Create a crontask to run this renew script every month:

$ echo "0 0 1 * * your_project_path/renew-certs.sh" >> /etc/crontab

Example: Deploy wordpress with free ssl certificate

This is a full example on how to deploy a wordpress + free ssl certificate.

Conclustion

There is many docker images that integrates the certbot mechanism. But I’ve prefered to understand the mechanism and to do it by myself.

If you have any problem, feel free to write in a comment. I will reply you ASAP.

Author

Omar Ghader

Fullstack web engineer and devops. Mobile web specialist