Hi Folks,

Today, I would like to write about how to do HTTPS for a website without buying a certificate and setting it up via your DNS provider. Let’s begin.

Abstract

What you will achieve by the end of this post: 

  • Every call to HTTP will be redirected to HTTPS via haproxy.
  • HTTPS will be served with Haproxy and LetsEncrypt as the Certificate provider.
  • Automatically update the certificate before its expiration.
  • No need for IPTable rules to route 8080 to 80.
  • Traffic to and from your page will be encrypted.
  • All this will cost you nothing.

I will use a static website generator for this called Hugo, which, if you know me, is my favorite generator tool. These instructions are for haproxy and hugo, if you wish to use apache and nginx for example, you’ll have to dig for the corresponding settings for letsencrypt and certbot.

What You Will Need

Hugo

You will need hugo, which can be downloaded from here: Hugo. A simple website will be enough. For themes, you can take a look at the humongous list located here: HugoThemes.

Haproxy

Haproxy can be found here: Haproxy. There are a number of options to install haproxy. I chose a simple apt-get install haproxy.

Let’s Encrypt

Information about Let’s Encrypt can be found on their website here: Let’s Encrypt. Let’s Encrypt’s client is now called Certbot which is used to generate the certificates. To get the latest code you either clone the repository Certbot, or use an auto downloader:

  1. user@webserver:~$ wget https://dl.eff.org/certbot-auto
  2. user@webserver:~$ chmod a+x ./certbot-auto
  3. user@webserver:~$ ./certbot-auto --help

Either way, I’m using the current latest version: v0.11.1.

Sudo

This goes without saying, but these operations will require you to have sudo privileges. I suggest staying in sudo for ease of use. This means that the commands I’ll write here will assume you are in sudo su mode thus no sudo prefix will be used.

Portforwarding

In order for your website to work under https, this guide assumes that you have port 80 and 443 open on your router / network security group.

Setup

Single Server Environment

It is possible for haproxy, certbot and your website to run on designated servers. Haproxy’s abilities allow you to define multiple server sources. In this guide, my haproxy, website and certbot will all run on the same server; thus redirecting to 127.0.0.1 and local IPs. This is more convenient, because otherwise the haproxy IP would have to be a permanent local/remote IP. Or an automated script would have to be setup, which is notified upon IP change and updates the IP records.

Creating a Certificate

Diving in, the first thing you will require is a certificate. A certificate will allow for encrypted traffic and an authenticated website. Let’s Encrypt is an independent, free, automated CA (Certificate Authority). Usually, the process would be to pay a CA to give you a signed, generated certificate for your website, and you would have to set that up with your DNS provider. Let’s Encrypt has that all automated, and free of charge. Neat.

Certbot

So let’s get started. Clone the repository into /opt/letsencrypt for further usage.

git clone https://github.com/certbot/certbot /opt/letsencrypt

Generating the certificate

Make sure that there is nothing listening on ports: 80, 443. To list usage:

  1. netstat -nlt | grep ':80\s'
  2. netstat -nlt | grep ':443\s'

Kill everything that might be on these ports, like apache2 and httpd. These will be used by haproxy and certbot for challenges and redirecting traffic.
You will be creating a standalone certificate. This is the reason we need port 80 and 443 open. Run certbot by defining the certonly and --standalone flags. For domain validation, you are going to use port 443, tls-sni-01 challenge. The whole command looks like this:

  1. cd /opt/letsencrypt
  2. ./certbot-auto certonly --standalon -d example.com -d www.example.com

If this displays something like, “couldn’t connect” you probably still have something running on a port it tried to use. The generated certificate will be located under /etc/letsencrypt/archive and /etc/letsencrypt/keys while /etc/letsencrypt/live is a symlink to the latest version of the cert. It’s wise to not copy these away from here, since the live link is always updated to the latest version. Our script will handle haproxy, which requires one cert file made from privkey + fullchain|.pem files.

Setup Auto-Renewal

Let’s Encrypt issues short lived certificates (90 days). In order to not have to do this procedure every 89 days, certbot provides a nifty command called renew. However, for the cert to be generated, the port 443 has to be open. This means, haproxy needs to be stopped before doing the renew. Now, you COULD write a script which stops it, and after the certificate has been renewed, starts it again, but certbot has you covered again in that department. It provides hooks called pre-hook and post-hook. Thus, all you have to write is the following:

  1. #!/bin/bash
  2.  
  3. cd /opt/letsencrypt
  4. ./certbot-auto renew --pre-hook "service haproxy stop" --post-hook "service haproxy start"
  5. DOMAIN='example.com' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/certs/$DOMAIN.pem'

If you would like to test it first, just include the switch --dry-run.
If it succeeds, you should see something like this:

  1. root@raspberrypi:/opt/letsencrypt# ./certbot-auto renew --pre-hook "service haproxy stop" --post-hook "service haproxy start" --dry-run
  2. Saving debug log to /var/log/letsencrypt/letsencrypt.log
  3.  
  4. -------------------------------------------------------------------------------
  5. Processing /etc/letsencrypt/renewal/example.com.conf
  6. -------------------------------------------------------------------------------
  7. Cert not due for renewal, but simulating renewal for dry run
  8. Running pre-hook command: service haproxy stop
  9. Renewing an existing certificate
  10. Performing the following challenges:
  11. tls-sni-01 challenge for example.com
  12. Waiting for verification...
  13. Cleaning up challenges
  14. Generating key (2048 bits): /etc/letsencrypt/keys/0002_key-certbot.pem
  15. Creating CSR: /etc/letsencrypt/csr/0002_csr-certbot.pem
  16. ** DRY RUN: simulating 'certbot renew' close to cert expiry
  17. **          (The test certificates below have not been saved.)
  18.  
  19. Congratulations, all renewals succeeded. The following certs have been renewed:
  20.   /etc/letsencrypt/live/example.com/fullchain.pem (success)
  21. ** DRY RUN: simulating 'certbot renew' close to cert expiry
  22. **          (The test certificates above have not been saved.)
  23. Running post-hook command: service haproxy start

Put this script into a crontab to run every 89 days like this:

  1. crontab -e
  2. # Open crontab for edit and paste in this line
  3. * * */89 * * /root/renew-cert.sh

And you should be all set. Now we move on to configure haproxy to redirect and to use our newly generated certificate.

Haproxy

Like I said, haproxy requires a single file certificate in order to encrypt traffic to and from the website. To do this, we need to combine privkey.pem and fullchain.pem. As of this post’s publication, there are a couple of solutions to automate this via a post hook on renewal. And also, there is an open ticket with certbot to implement a simpler solution located here: https://github.com/certbot/certbot/issues/1201. I, for now, have chosen to simply concatenate the two files together with cat like this:

DOMAIN='example.com' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/certs/$DOMAIN.pem'

It will create a combined cert under /etc/haproxy/certs/example.com.pem.

Haproxy configuration

If haproxy happens to be running, stop it with service haproxy stop.
First, save the default configuration file: cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.old. Now, overwrite the old one with this new one (comments about what each setting does, are in-lined; they are safe to copy):

  1. global
  2.     daemon
  3.     # Set this to your desired maximum connection count.
  4.     maxconn 2048
  5.     # https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#3.2-tune.ssl.default-dh-param
  6.     # bit setting for Diffie - Hellman key size.
  7.     tune.ssl.default-dh-param 2048
  1. defaults
  2.     option forwardfor
  3.     option http-server-close
  1.     log     global
  2.     mode    http
  3.     option  httplog
  4.     option  dontlognull
  5.     timeout connect 5000
  6.     timeout client  50000
  7.     timeout server  50000
  8.     errorfile 400 /etc/haproxy/errors/400.http
  9.     errorfile 403 /etc/haproxy/errors/403.http
  10.     errorfile 408 /etc/haproxy/errors/408.http
  11.     errorfile 500 /etc/haproxy/errors/500.http
  12.     errorfile 502 /etc/haproxy/errors/502.http
  13.     errorfile 503 /etc/haproxy/errors/503.http
  14.     errorfile 504 /etc/haproxy/errors/504.http
  1. # In case it's a simple http call, we redirect to the basic backend server
  2. # which in turn, if it isn't an SSL call, will redirect to HTTPS that is
  3. # handled by the frontend setting called 'www-https'.
  4. frontend www-http
  5.     # Redirect HTTP to HTTPS
  6.     bind *:80
  7.     # Adds http header to end of end of the HTTP request
  8.     reqadd X-Forwarded-Proto:\ http
  9.     # Sets the default backend to use which is defined below with name 'www-backend'
  10.     default_backend www-backend
  1. # If the call is HTTPS we set a challenge to letsencrypt backend which
  2. # verifies our certificate and then directs traffic to the backend server
  3. # which is the running hugo site that is served under https if the challenge succeeds.
  4. frontend www-https
  5.     # Bind 443 with the generated letsencrypt cert.
  6.     bind *:443 ssl crt /etc/haproxy/certs/skarlso.com.pem
  7.     # set x-forward to https
  8.     reqadd X-Forwarded-Proto:\ https
  9.     # set X-SSL in case of ssl_fc <- explained below
  10.     http-request set-header X-SSL %[ssl_fc]
  11.     # Select a Challenge
  12.     acl letsencrypt-acl path_beg /.well-known/acme-challenge/
  13.     # Use the challenge backend if the challenge is set
  14.     use_backend letsencrypt-backend if letsencrypt-acl
  15.     default_backend www-backend
  1. backend www-backend
  2.    # Redirect with code 301 so the browser understands it is a redirect. If it's not SSL_FC.
  3.    # ssl_fc: Returns true when the front connection was made via an SSL/TLS transport
  4.    # layer and is locally deciphered. This means it has matched a socket declared
  5.    # with a "bind" line having the "ssl" option.
  6.    redirect scheme https code 301 if !{ ssl_fc }
  7.    # Server for the running hugo site.
  8.    server www-1 192.168.0.17:8080 check
  1. backend letsencrypt-backend
  2.    # Lets encrypt backend server
  3.    server letsencrypt 127.0.0.1:54321

Save this, and start haproxy with services haproxy start. If you did everything right, it should say nothing. If, however, something went wrong with starting the proxy, it usually displays something like this:

Job for haproxy.service failed. See 'systemctl status haproxy.service' and 'journalctl -xn' for details.

You can also gather some more information on what went wrong from less /var/log/haproxy.log

Starting the Server

Everything should be ready to go. Hugo has the concept of a baseUrl. Everything that it loads and tries to access will be prefixed with it. You can either set it through it’s config.yaml file, or from the command line.

To start the server, call this from the site’s root folder:

hugo server --bind=192.168.x.x --port=8080 --baseUrl=https://example.com --appendPort=false

An interesting thing to note here is https and the port. The IP could be 127.0.0.1 as well. I experienced problems though with not binding to network IP when I was debugging the site from a different laptop on the same network.
Once the server is started, you should be able to open up your website from a different browser, not on your local network, and see that it has a valid certificate installed. In Chrome you should see a green icon telling you that the cert is valid.

Last Words

And that is all. The site should be up and running and the proxy should auto-renew your site’s certificate. If you happen to change DNS or change the server, you’ll have to reissue the certificate.
Thanks for reading! Any questions or trouble setting something up, please feel free to leave a comment.

Cheers,
Gergely.