Setting up SSL encryption using Reverse Proxy on Raspberry Pi

Setting up SSL encryption using Reverse Proxy on Raspberry Pi

As this post title suggest, this gon’ be a major headache from start to finish. This is hopefully an improvement on other tutorials and will make the process of implementing a containerized reverse proxy on Raspberry Pi easier for you.

Have a look at Jason Wilder’s excellent blog post titled Automated Nginx Reverse Proxy using Docker for some background information I am not going to explain here.

In an attempt to secure access to my blog and other services running on my Raspberry Pi, I looked into Let’s Encrypt, a certificate authority offering free SSL certificates. They offer a client program called certbot which implements an automated process allowing you to retrieve and renew certificates automatically. Initially, I thought what I needed was a “letsencrypt docker image” to generate and manage SSL certificates. Having tried all images that come up on Google, I realized that a better description of what we are trying to achieve is a reverse proxy with letsencrypt support”. This search led me to find Traefik.

Some background (optional read)

I wrote this section to show how much patience it took to arrive at a working solution and how much time you’ll save following this tutorial.

There are countless tutorials and docker images on Docker Hub for the purpose of running a certbot/letsencrypt container and encrypting access to your Raspberry Pi. The following is a list of problems I encountered:

  1. Docker images are not compiled for my target architecture (ARM), meaning I would’ve had to compile them myself which always results in wasting lots of time rewriting and testing Dockerfiles.
  2. The supplied documentation contains plotholes or is unclear.
  3. No documentation supplied at all.
  4. I couldn’t even get the sample set ups to work.

I spent a good couple of hours on attempting to get the following images to work:

ImageReason for rejection
nginx-proxyNot ARM compatible
rpi-nginx-proxySame as previous image, but built for Raspberry Pi. Followed lroguet’s blog post, the base image instructions, without success.

Introducing Traefik

Træfik (pronounced like traffic) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. It supports several backends (Docker, Swarm mode, […] and a lot more) to manage its configuration automatically and dynamically.

Traefik is as awesome as it is misspelled. It’s a reverse proxy server with a Raspberry Pi compatible docker image that has built in support for LetsEncrypt. It also happens to include good documentation that helped me get it up and running in no time! What’s amazing about it is its automatic ability to retrieve and validate Bitdefender verified SSL certificates for all subdomains. This is exactly what I needed and quite possibly why you are reading this post in the first place.

Reverse Proxy and Docker networks

I don’t use the term reverse proxy lightly and neither should you. Even though the concept is simple, it can be an absolute pain in the butt to set up. This section explains some of the concepts that will help you understand how a reverse proxy functions in Docker.

Reverse Proxy

Traefik Diagram The reverse proxy is a Docker container through which all external internet traffic is routed before it hits your service containers. Since there is a single external point of contact, we can use this server to do things like:

  • load balancing between multiple backend containers running the same image (like a web application called backoffice),
  • route subdomains to different containers running different services (api.example.com, web.example.com)
  • manage SSL certificate retrieval and renewal for subdomains
  • encrypt all internet traffic before it leaves the network

Docker Networks

Docker has the ability to host containers on virtualized networks. Containers connected to a network can only communicate with other containers on that same network (which is how physical networks work, I suppose…) We can use this to our advantage because we can run all publicly accessable containers on a custom web network, and everything else on a private network. This increases the security of your system because webserver containers can not access the private network.

Instructions

Once I sorted out all the kinks in my configuration, getting Traefik to work was comparatively easy. I aim to make this as quick and painless as possible. You need a minimum of 2 containers:

  1. Treafik container
  2. Backend container hosting an application (nginx test server, in our simple case)

Prerequisites

  • create a new directory to host your config master
  • create a master/html directory and place a test index.html file in there with some test content.
  • create a master/traefik directory and create the following files: acme.json, treafik.toml
  • create a docker compose file master/docker-compose.yaml
  • create a docker network using docker network create web
master/
├── docker-compose.yaml
├── html
│   └── index.html
└── traefik
    ├── acme.json
    └── traefik.toml

The reason our parent is called master is because you can change your docker-compose.yaml later to include your entire Rpi docker container setup.

One-second Setup Script

mkdir master master/html master/traefik
cd master
touch docker-compose.yaml traefik/traefik.toml traefik/acme.json html/index.html
docker network create web

The contents of each file are listed in the following sections.

traefik.toml

  • This configuration redirects http traffic to https, serving encrypted content whenever possible.
  • The Traefik dashboard runs on port 8080 and;
  • The [docker] section tells Traefik that we are using Docker containers as our backend.
  • The exposedbydefault flag is set to false because we want to specify which backend containers are accessible from the internet.
  • To retrieve SSL certificates for your domains, list them in the [[acme.domains]] section (feel free to add more).
debug = false
checkNewVersion = true
logLevel = "DEBUG"
defaultEntryPoints = ["https","http","ws","wss"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]

[retry]
[web]
address = ":8080"

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "<example.com>"
watch = true
exposedbydefault = false

[acme]
email = "<your email>"
storage = "acme.json"
entryPoint = "https"
OnHostRule = true

[[acme.domains]]
  main = "blog.example.com"
[[acme.domains]]
  main = "db.example.com"

Complete Docker Compose File

This is the final docker-compose.yaml file (for easy copy/paste). I will go in more details in the following sections.

  • The networks sections tells docker to use the custom created web network, rather than automatically creating new ones at run time.
version: '3.3'

networks:
  web:
    external: true

services:
# TREAFIK
  traefik:
    image: traefik
    restart: always
    ports:
      - 8080:8080
      - 80:80
      - 443:443
    networks:
      - web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/traefik.toml:/traefik.toml
      - ./traefik/acme.json:/acme.json
    container_name: traefik

# TEST WEB SERVER
  web:
    container_name: nginx-test
    image: lroguet/rpi-nginx:latest
    networks:
      - web
    expose:
      - 80
    volumes:
      - ./html:/var/www/html
    labels:
      - "traefik.backend=test-webserver"
      - "traefik.docker.network=web"
      - "traefik.frontend.rule=Host:blog.example.com"
      - "traefik.enable=true"
      - "traefik.port=80"
      - "traefik.default.protocol=http"

Step 1: Create a Traefik container

The treafik service is configured as follows:

  • ports: We are exposing the http, https and dashboard interface ports.
  • networks: In order to route internet traffic to our backend container, we have to make the container part of the web network.
  • volumes: The container needs access to its config files as well as the docker.sock file in order to communicate with the Docker API and be notified when containers are started and stopped.

Step 2: Create a backend service:

Now we need to set up a backend service to which traffic is routed. Let’s start with a simple example using a basic nginx webserver that serves the example HTML file you created before.

There are two configuration items we need to add to a custom backend container for Traefik1.

  • use the expose tag to expose any service ports to the Docker network. We have to use expose rather than ports because Traefik forwards HTTP traffic via the Docker network.
  • we add container specific configuration using the labels tag.
      labels:
        - "traefik.backend=test-webserver"                # give backend service a name
        - "traefik.docker.network=web"                    # Tell Traefik which network this container resides on
        - "traefik.frontend.rule=Host:blog.example.com"   # Forward requests to blog.example.com to this container
        - "traefik.enable=true"                           # Explicitly enabled access to this container through proxy.
        - "traefik.port=80"                               # service port on backend container
        - "traefik.default.protocol=http"                 # whether to communicate in `http` or `https` internally.
    

    Read the official documentation for the various container configuration labels Traefik supports.

If you previously followed my DDNS Tutorial you would have set up web forwarding rules which map a subdomain to a specific port on your server. Since we are now using a reverse proxy, we are doing this routing ourselves. Remove those port mapping rules on Dynu. They are no longer required

  1. I hope by now reading this word is slowing down your reading pace as much as it is inhibiting my typing abilities. ↩︎