about summary refs log tree commit diff
path: root/content/blog/tailscale-docker-https.md
blob: 9a836d98c9d1b5bb5aa27e32bc7df0e273335284 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
---
title: Tailscale, Docker and HTTPS
date: 2021-12-29
---

I run a number of services in my home network. For the majority of these services, I don't want to make them available on the internet, I want to only be able to access them when I'm on my home network. However, sometimes I'm not at home and I still want to access them. So far I've been using plain [wireguard](https://www.wireguard.com/) to achieve this. While the initial configuration for wireguard is pretty simple, it starts to be a bit more cumbersome as I add more hosts/containers. It's also not easy to share keys with other folks if I want to give access to some of the machines or services. For that reason I decided to give a look at [tailscale](https://tailscale.com/).

There's already a lot of articles about tailscale and how to use and configure it. Their [documentation](https://tailscale.com/kb/) is also pretty good, so I won't cover the initial setup.

As stated above, I want to access some of my services that are running as docker containers from anywhere. For web services, I want to use them through HTTPS, with a valid certificate, and without having to remember on which port the service it's listening. I also don't want to setup a PKI in my home lab for that (and I'm also not interested in configuring split DNS), and instead I prefer to use [let's encrypt](https://letsencrypt.org/) with a proper subdomain that is unique for each service.

The [tailscale documentation](https://tailscale.com/kb/1054/dns/) has two suggestions for this:

- use their magicDNS feature / split DNS
- setup a subdomain on a public domain

Since I already have a public domain that I use for my home network, I decided to go with the second option (I'm also uncertain how to achieve my goal using magicDNS without running tailscale inside the container).

The public domain I'm using is managed through [Google Cloud Domain](https://cloud.google.com/dns/docs/tutorials/create-domain-tutorial). I create a new record for the services I want to run (for example, `dash` for my instance of grafana), using the IP address from the tailscale node the service runs on (e.g. 100.83.51.12).

For routing the traffic I use [traefik](https://traefik.io/). The configuration for traefik looks like this:

    global:
      sendAnonymousUsage: false
    providers:
      docker:
        exposedByDefault: false
    entryPoints:
      http:
        address: ":80"
      https:
        address: ":443"
    certificatesResolvers:
      dash:
        acme:
          email: franck@fcuny.net
          storage: acme.json
          dnsChallenge:
            provider: gcloud

The important bit here is the `certificatesResolvers` part. I'll be using the [dnsChallenge](https://doc.traefik.io/traefik/user-guides/docker-compose/acme-dns/) instead of the [httpChallenge](https://doc.traefik.io/traefik/user-guides/docker-compose/acme-http/) to obtain the certificate from let's encrypt. For this to work, I need to specify the `provider` to be [gcloud](https://go-acme.github.io/lego/dns/gcloud/). I'll also need a service account (see [this doc](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application) to create it). I run `traefik` in a docker container, and the `systemd` unit file is below. The required bits for using the `dnsChallenge` with `gcloud` are:

- the environment variable `GCP_SERVICE_ACCOUNT_FILE`: it contains the credentials so that `traefik` can update the DNS record for the challenge
- the environment variable `GCP_PROJECT`: the name of the GCP project
- mounting the service account file inside the container (I store it on the host under `/data/containers/traefik/config/sa.json`)

```systemd
[Unit]
Description=traefik proxy
Documentation=https://doc.traefik.io/traefik/
After=docker.service
Requires=docker.service

[Service]
Restart=on-failure
ExecStartPre=-/usr/bin/docker kill traefik
ExecStartPre=-/usr/bin/docker rm traefik
ExecStartPre=/usr/bin/docker pull traefik:latest

ExecStart=/usr/bin/docker run \
 -p 80:80 \
 -p 9080:8080 \
 -p 443:443 \
 --name=traefik \
 -e GCE_SERVICE_ACCOUNT_FILE=/var/run/gcp-service-account.json \
 -e GCE_PROJECT= gcp-super-project \
 --volume=/data/containers/traefik/config/acme.json:/acme.json \
 --volume=/data/containers/traefik/config/traefik.yml:/etc/traefik/traefik.yml:ro \
 --volume=/data/containers/traefik/config/sa.json:/var/run/gcp-service-account.json \
 --volume=/var/run/docker.sock:/var/run/docker.sock:ro \
 traefik:latest
ExecStop=/usr/bin/docker stop traefik

[Install]
WantedBy=multi-user.target
```

As an example, I run [grafana](https://grafana.com/) on my home network to view metrics from the various containers / hosts. Let's pretend I use `example.net` as my domain. I want to be able to access `grafana` via <https://dash.example.net>. Here's the `systemd` unit configuration I use for this:

    [Unit]
    Description=Grafana in a docker container
    Documentation=https://grafana.com/docs/
    After=docker.service
    Requires=docker.service

    [Service]
    Restart=on-failure
    RuntimeDirectory=grafana
    ExecStartPre=-/usr/bin/docker kill grafana-server
    ExecStartPre=-/usr/bin/docker rm grafana-server
    ExecStartPre=-/usr/bin/docker pull grafana/grafana:latest

    ExecStart=/usr/bin/docker run \
      -p 3000:3000 \
      -e TZ='America/Los_Angeles' \
      --name grafana-server \
      -v /data/containers/grafana/etc/grafana:/etc/grafana \
      -v /data/containers/grafana/var/lib/grafana:/var/lib/grafana \
      -v /data/containers/grafana/var/log/grafana:/var/log/grafana \
      --user=grafana \
      --label traefik.enable=true \
      --label traefik.http.middlewares.grafana-https-redirect.redirectscheme.scheme=https \
      --label traefik.http.middlewares.grafana-https-redirect.redirectscheme.permanent=true \
      --label traefik.http.routers.grafana-http.rule=Host(`dash.example.net`) \
      --label traefik.http.routers.grafana-http.entrypoints=http \
      --label traefik.http.routers.grafana-http.service=grafana-svc \
      --label traefik.http.routers.grafana-http.middlewares=grafana-https-redirect \
      --label traefik.http.routers.grafana-https.rule=Host(`dash.example.net`) \
      --label traefik.http.routers.grafana-https.entrypoints=https \
      --label traefik.http.routers.grafana-https.tls=true \
      --label traefik.http.routers.grafana-https.tls.certresolver=dash \
      --label traefik.http.routers.grafana-https.service=grafana-svc \
      --label traefik.http.services.grafana-svc.loadbalancer.server.port=3000 \
      grafana/grafana:latest

    ExecStop=/usr/bin/docker stop unifi-controller

    [Install]
    WantedBy=multi-user.target

Now I can access my grafana instance via HTTPS (and <http://dash.example.net> would redirect to HTTPS) while my tailscale interface is up on the machine I'm using (e.g. my desktop or my phone).