Secure Hosting with GitLab Pages and Let’s Encrypt

, ,

I’ve finally acted on my promise from 2016 to enable HTTPS on this blog, so your connection to this website is now encrypted.

Change of Plans

In contrast to my original plan of using GitHub Pages in connection with CloudFlare, I’ve decided to use GitLab Pages in connection with Let’s Encrypt for obtaining the TLS/SSL certificate. While GitHub Pages can be set up more easily, GitLab Pages has the advantage that you are not tied to a specific site generator or build process: you just describe how your site is built from the sources in a GitLab CI configuration and the output of that process is then copied to the web server. In particular, even if you use Jekyll as you would normally do with GitHub Pages, you can use any plugin you want and not just the ones that are integrated into GitHub Pages.

TLS support for GitLab Pages

Unfortunately, configuring TLS for your custom domain is more tricky with GitLab Pages than with GitHub Pages, where TLS is now standard for custom domains with automatic certificate management: With GitLab Pages you first need to obtain a TLS certificate for your domain, which you can then upload to GitLab.com (including the private key, of course). So how do you get a valid TLS certificate for your domain if you do not want to pay a substantial fee to one of the corporate certificate authorities?

Let’s Encrypt

In order to boost encryption on the web, the Internet Security Research Group (ISRG) has founded a certificate authority which offers TLS certificates for free: Let’s Encrypt. The catch is that these certificates are quite short lived (they expire after 90 days), so your certificate needs to be renewed frequently.

While GitLab is working on integrating Let’s Encrypt into GitLab Pages, you currently need to generate and renew the certificate by yourself and upload it to your GitLab Pages configuration. Fortunately, by using GitLab’s API for uploading the certificate from within a scheduled job in GitLab CI, this process can be fully automated.

Obtaining a TLS certificate from Let’s Encrypt

But first, let us review how you can obtain a certificate for your domain from Let’s Encrypt. Since Let’s Encrypt is offering certificates for free and requires certificates to be renewed frequently, obtaining and renewing a certificate needs to work without labour-intensive manual intervention, so that the process is fully automatic: Using a command-line tool called certbot, you send a request for obtaining a certificate to Let’s Encrypt which returns a challenge for proving that you’re in control of the domain for which the certificate is requested (authentication). After completing the challenge successfully, Let’s Encrypt responds with the certificate (the public key including the full certificate chain and the private key) so you can set up your web server to serve that certificate to the user (installation). In fact, depending on which plugin you use (certbot comes with plugins e.g. for Apache and Nginx), certbot performs authentication and installation by itself under the cover, so that you do not need to cope with that yourself.

If you use GitLab pages, you do not have full control over the web server so that most of the plugins will not be useful to you. However, if you are lucky enough that your DNS provider offers an API to modify DNS entries, you can use one of the DNS plugins for authentication (if not, you can still use manual mode, but that is harder to automate). Here the challenge is to add a specific DNS entry to your domain. Fortunately, my DNS provider INWX offers such an API and a certbot plugin is available on GitHub, installable as a Python package via pip. As for installation, we will use the certonly command of certbot to generate and retrieve the certificate only but skip installation; we can then upload and configure the generated certificate using GitLab’s API.

Putting it all together

We are now able to set up a job called update_cert in GitLab CI that requests a certificate from Let’s Encrypt and uses GitLab’s API to configure it on the web server. Since on GitLab.com, GitLab CI only supports the Docker executor, we need to specify a Docker image first, and we will use the official certbot Docker image for that.

update_cert:
  image:
    name: certbot/certbot
    entrypoint: [""]

We need to overwrite the default entrypoint so that we have shell access. The certbot image is based on the official python image (python:2-alpine3.7 to be precise) so that we can use pip for installing Python packages. Next we define two variables which contain the path where certbot will put the resulting certificate, respectively the private key.

  variables:
    CERT_FILE: "/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
    KEY_FILE: "/etc/letsencrypt/live/$DOMAIN/privkey.pem"

DOMAIN is a variable that points to your domain (i.e. in our case www.ummels.de). Next, in order to use certbot with INWX, we need to install the inwx DNS plugin and write a configuration file containing the credentials for accessing INWX’s API. The credentials itself are passed in as protected variables which we can configure in GitLab’s UI, so that they are hidden from somebody browsing your repository. We also need to install curl so that we can access GitLab’s API later on.

  before_script:
    - apk add --no-cache curl
    - pip install https://github.com/oGGy990/certbot-dns-inwx/archive/master.zip
    - echo "certbot_dns_inwx:dns_inwx_url = https://api.domrobot.com/xmlrpc/" > inwx.cfg
    - chmod 600 inwx.cfg
    - echo "certbot_dns_inwx:dns_inwx_username = $INWX_USER" >> inwx.cfg
    - echo "certbot_dns_inwx:dns_inwx_password = $INWX_PASSWORD" >> inwx.cfg
    - echo "certbot_dns_inwx:dns_inwx_shared_secret = optional" >> inwx.cfg

Now for the main script: First we call certbot with the certonly and the required options to generate the certificate (see Running with Docker). Then we use curl for uploading the resulting certificate to GitLab using their API.

  script:
    - certbot certonly -n --agree-tos -a certbot-dns-inwx:dns-inwx -d $DOMAIN -m $GITLAB_USER_EMAIL --certbot-dns-inwx:dns-inwx-credentials inwx.cfg
    - "curl --silent --fail --show-error --request PUT --header \"Private-Token: $GITLAB_TOKEN\" --form \"certificate=@$CERT_FILE\" --form \"key=@$KEY_FILE\" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pages/domains/$DOMAIN"

Finally, we configure the job so that it only runs as part of a scheduled pipeline (or on manual execution) and not as part of the normal integration build.

  only:
    - schedules

Since the script can be used for obtaining a new certificate as well as for replacing an existing one, we just need to set up a scheduled pipeline that runs e.g. on the 1st of every even month in order to always serve a valid certificate.

Wrap-Up

The full GitLab CI configuration including the job to build the website after a push to the master branch can be found here.