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. Personnally, I deploy it on my Orange pi.
Buy it from Aliexpress: Orange pi
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
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.