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.

Going Static

,

After almost 10 years, I have decided to ditch Wordpress for Jekyll, a static blog engine. As opposed to Wordpress, Jekyll is not dependent on MySQL and PHP, but simply generates static HTML files from sources in Markdown, so you can create your blog posts comfortably using your favourite text editor, let Jekyll generate the HTML and put it on an arbitrary web server. As the server only needs to serve the HTML pages, the attack surface is greatly reduced.

Like Wordpress, Jekyll boasts a huge number of themes, of which I have chosen the excellent Minimal Mistakes theme in order to create a fresh but familiar look for this blog.

Since many image links in older posts were broken due to an outdated Gallery installation, I have decided to only convert posts from 2014 or later. Moreover, while I was able to copy the comments from these posts, I have not yet decided how to let people submit new comments since this requires at least some dynamic server code. Hence, at the moment it is not possible to post comments on this blog.

After restoring comment support, I am planning to move the blog over to GitHub Pages and CloudFlare GitLab Pages, so I can finally serve this site over HTTPS. For now you can already browse the complete source code on GitHub GitLab.

Tramino Braunschweig

,

After some delays due to certification, the new Tramino trams for Braunschweig have finally entered into service last Monday. The first two pictures show unit 1451 running on line M5.

Tramino Braunschweig Hauptbahnhof

Tramino Braunschweig Ägidienkirche

As can be seen on the next picture, the interior looks quite modern as well.

Tramino Braunschweig Interior

Green lights above the doors indicate that the vehicle may be entered.

Tramino Braunschweig Heinrich-Büssing-Ring

The next picture shows unit 1451 standing next to one of its ancestors built in 1995.

Tramino Braunschweig Heinrich-Büssing-Ring 2

Finally, a bonus picture of a tram from the 2007 series coupled to a trailer built in 1974! These trailers will probably be retired as soon as enough new trams have entered into service.

Braunschweig Hauptbahnhof