This post goes into the details of how to make websites you run in your home, such as Home Assistant, exposed to the public securely and (almost) for free.
The post details how to set up a free GCE instance, TLS certificates via LetsEncrypt, a Treafik proxy with Google authentication, and automatic VPNs built using Wireguard. For brevity, I called this setup Wiregate, i.e. a wire-guard gateway.
In this post we will walk through the set up of the VM, configuration of DNS, Letsencrypt TLS for wildcard DNS certificates, configuration of Traefik and google Auth, and set up of Wiregate DNS. We’ll finish with configuring a Plex media server to be Google-auth accessible via plex.example.com
. If you want to continue to the Home Assistant guide, see another post.
Getting a Google Cloud project
Google Cloud is now an umbrella for multiple projects. Not only does it allow you to add additional Google Authentication endpoints (which we will need), control your domain (Google DNS), but it also give you a free tier that includes a f1-micro
instance in one of the US west zones.
Unfortunately, not everything is included in the free tier, and in order to be able to use it, you will need a credit card to set up billing. However, don’t fret, as the whole thing is pretty cheap to run. I pay ~0.05USD a month for the whole setup below.
For the rest of the post, I’ll be using <myproject>
whenever you should use your own project name.
Getting a domain
First, you’ll need a domain name. You can get a relatively good name either at Uniregistry or Google Domains for pretty cheap. For example mylittlecutehouse.uk
costs ~10GBP a year.
To make things easier, you should transfer your domain for control to Google Cloud DNS, follow these instructions
For the rest of the post, I’ll use example.com
for the suffix.
Setting a GCE instance
Using the f1-micro
instances of GCE free tier, we’ll set up an Ubuntu 20.04 LTS instance in the cloud acting as our Cloud endpoint.
Yes, this is not a particularly HA set up, but with GCE’s live-migration downtime is not an issue for this non-bussiness critical setup.
We’ll chose us-east1
as it is the closest free tier region to Europe. Since f1-micro
is extremely limited in terms of RAM available (600MB), we will only be running a minimal instance with Go-based programms that don’t require a lot of memory.
Reserve the public IP address
Public IP addresses, wether static or ephemeral cost nothing when used. Let’s get a regional one.
gcloud compute addresses create wiregate
--project=<myproject> --description="Public IP for wiregate." --network-tier=STANDARD --region=us-east1
Take not of the IP address as you will be referring to it later on. For our purposes let’s assume its 35.207.1.1
.
Now, update the A
record of *.example.com
in your Google DNS to point to that IP address.
Create the instance
We’ll create an instance with a relatively large disk (25GB) and bound to the public IP --address=35.207.1.1
.
gcloud beta compute --project=<myproject> instances create wiregate --zone=us-east1-c --machine-type=f1-micro --subnet=default --address=35.207.1.1 --network-tier=STANDARD --maintenance-policy=MIGRATE --tags=http-server,https-server --image= ubuntu-minimal-2004-focal-v20200423 --image-project=ubuntu-os-cloud --boot-disk-size=25GB --boot-disk-type=pd-standard --boot-disk-device-name=wiregate --no-shielded-secure-boot --shielded-vtpm --shielded-integrity-monitoring --reservation-affinity=any
Install all the other necessary tools.
As we’ve chosen the minimal install, there’s almost nothing on the system.
apt-get install iputils-ping
apt-get install vim
Set up etckeeper (optional)
Etckeeper allows you to keep all your etc configuration in git.
First, you need to use the root account, as all etc git changes will be triggered from the root
account.
sudo su
apt-get -y install etckeeper
eval `ssh-agent -s`
Generage an SSH key for the root
user and add it to your Github or Gitlab profile.
etckeeper init
git remote add origin git@<somewhere>
etckeeper commit "Initial commit."
git push --set-upstream origin master
Set up Docker
Since we’ll be running containers, getting Docker to run is a good idea.
sudo apt-get install docker.io
sudo docker version
Setting up Traefik
Traefik is a very flexible, yet lightweight, reverse proxy written in Go. Look out: a lot of example configuration for traefik is meant for v1. This writeup is aimed at v2.2.
Adding extra firewall rules in GCE
Traefik has a debug interface that is visible on 8080
. While not strictly needed, it may be useful to have it available during setup. In order to allow 8080
in, we need to add a firewall rule for VM instances matching allow-tcp-8080
by creating the following.
gcloud compute --project=<myproject> firewall-rules create allow-debug-traefik --description="Allows debug Traefik port of 8080" --direction=INGRESS --priority=1000 --network=default --action=ALLOW --rules=tcp:8080 --source-ranges=0.0.0.0/0 --target-tags=allow-tcp-8080
Also we want to make sure to add the allow-tcp-8080
to “Network tags” of the instance (by editing it), alongside the project default http-server
and https-server
. Test the whole thing running
sudo nc -l 8080
and curling it from somewhere.
When you’re done, remove the 8080
firewall rule from the machine.
Simple Traefik with basic TLS
This section sets up Traefik as a systemd
service and configures LetsEncrypt certificate autorenwal for a wiregate.example.com
domain exposing the debug webpage of Traefik over TLS.
First, let’s create a config directory for Traefik.
sudo mkdir -p /etc/traefik
Static configuration file
Edit /etc/traefik/traefik.toml
to create the static configuration. Static configuration is read at Treafik startup and configures mostly ports and other sources of configration (a.k.a. “providers”)
[log]
level = "DEBUG"
[api]
dashboard = true
debug = false
insecure = true # disable me
[ping]
entryPoint = "traefik"
[accessLog]
[entryPoints]
[entryPoints.web-insecure]
address = ":80"
[entryPoints.web-insecure.http]
[entryPoints.web-insecure.http.redirections]
[entryPoints.web-insecure.http.redirections.entryPoint]
to = "web-secure"
scheme = "https"
[entryPoints.web-secure]
address = ":443"
[entryPoints.traefik]
address = ":8080"
# Note: specifying the provider on the command line doesn't work. This needs to be a docker-local path.
[providers]
[providers.file]
filename = "/conf/traefik.dynamic.toml"
[certificatesResolvers]
[certificatesResolvers.simpletls.acme]
email = "myemail@example.com" # change me
storage = "acme.json"
[certificatesResolvers.simpletls.acme.tlsChallenge]
Dynamic configuration file
Now, edit /etc/traefik/traefik.dynamic.toml
to create the dynamic file-based configuration “provider”. This will be read dynamically by a running Traefik. Look out: any errors in this will mean that none of your routes will work. If in doubt always visit wiregate.example.com
to see whether that is working.
[http.routers]
[http.routers.simple]
entryPoints = ["web-secure"]
rule = "Host(`wiregate.example.com`)"
service = "api@internal"
[http.routers.simple.tls]
certresolver = "simpletls"
In this configuration we simply tell Traefik to allow wiregate.example.com
and show the internal dashboard on it.
The systemd service file
In the file /etc/systemd/system/traefik.service
This mounts the whole /etc/traefik
directory under /conf
inside the docker container and also serves /etc/traefik/traefik.env` as the environment file.
[Unit]
Description=Traefik Frontend Server
After=docker.service network-online.target
[Service]
Restart=always
EnvironmentFile=/etc/traefik/traefik.env
ExecStart=/usr/bin/docker run \
--attach stderr --attach stdout \
--volume /etc/traefik:/conf \
--env-file=/etc/traefik/traefik.env \
--net=host \
traefik:v2.2 \
--configFile=/conf/traefik.toml
Restart=always
ExecStop=/usr/bin/docker stop -t 2 traefik
[Install]
WantedBy=multi-user.target
Please note the explicit Docker tag of :v2.2
to avoid unexpected auto-updates.
Run
sudo systemctl daemon-reload
sudo systemctl start traefik
sudo systemctl enable traefik
Setting wildcard DNS for Let’s Encrypt using Google DNS
In order to support wildcard certificates, Traefik needs to be be able to modify DNS entries via Google API calls.
Typically in order to use DNS-01 with Google DNS, you need to provide a Google service account via GCE_SERVICE_ACCOUNT_FILE
env variable. However, since we’re running a VM on GCE, we can use the Metadata service to serve a service account that is appropriate.
First we need to set up a new service account that will have access to DNS configuration. For that, click here. Select a role: DNS Administrator.
This will create a service account that has DNS writing permissions. Now, edit VM’s Service Account to select the one you just created.
Since we’re running in the same project, the GCE Metadata service should populate both the GCE_PROJECT
and GCE_SERVICE_ACCOUNT_FILE
of thee configuration
Traefik automatically discovers what DNS names it needs by inspecting the Routes, which are stored in the dynamic configuration file. As such we will need to change both the static and dynamic config files.
Change the static traefik.toml
TLS-related section to:
[certificatesResolvers]
[certificatesResolvers.wildcardtls.acme]
email = "myemail@example.com" # changeme
storage = "/conf/acme.json"
[certificatesResolvers.wildcardtls.acme.dnsChallenge]
resolvers = ["8.8.8.8:53", "1.1.1.1:53"]
provider = "gcloud"
# Note: normally we would need to provide ENV variables GCE_PROJECT and GCE_SERVICE_ACCOUNT_FILE.
# However we are running on GCE with a service account that has DNS permissions on the VM, so these will
# be sorted out automatically from GCE Metadata service.
Note, the /etc/traefik/acme.json
contains secrets. Add it to .gitignore
in the same dir so that etckeeper won’t be checking them in.
Now we need to specify the DNS addressess we want. For that, we need to use a SANS field inside a route. Modify the dynamic profier file of traefik.dynamic.toml
to:
[http.routers]
[http.routers.simple]
rule = "Host(`wiregate.example.com`)"
service = "api@internal"
[http.routers.simple.tls]
certresolver = "wildcardtls"
[[http.routers.simple.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
Google Authentication proxy using traefik-forward-auth
Traefik is very flexible, and one of its capabilities is to have pluggable authentication systems. One of them is traefik-forward-auth
. It can intercept any browser-based session and require the user to log in with a whitelisted Google account (or any other OpenID Connect provider).
The setup is relatively complicated: it requires traefik-forwad-auth
to run alongside Traefik, with the latter having a middleware configured for all routes that are meant to be authenticated.
OAuth2 credentials
As with any OAuth2 flow, we need to tell the upstream authentication provider who we are.
For that we can use the IAM configuration panel of or Google Cloud project. We’ll need to create a OAuth consent screen and new set of credentials. Follow the upstream instructions on how to do this.
Configuring traefik-forward-auth
We will use a file0based configuration placing it in /etc/traefik/traefik-forward-auth.ini
Please read the below file carefuly, as it really has things left out.
# Run in separate-host configuration, issuing auth for all of *.example.com.
auth-host=auth.example.com
cookie-domain=example.com
default-provider=google
providers.google.client-id= # what Google Console told you
providers.google.client-secret= #what Google console told you
# Repeated list of allowed Google accounts.
whitelist= #email @gmail.com
whitelist= #email @gmail.com
# Secret for signing cookies.
secret= # generate using `openssl rand -hex 16`
Configuring the systemd service
The service file should be put in /etc/systemd/system/traefik-forward-auth.service
[Unit]
Description=Traefik Forward Auth Server
After=docker.service network-online.target
[Service]
Restart=always
EnvironmentFile=/etc/traefik/traefik-forward-auth.env
ExecStart=/usr/bin/docker run \
--attach stderr --attach stdout \
--volume /etc/traefik:/conf \
--env-file=/etc/traefik/traefik-forward-auth.env \
--net=host \
thomseddon/traefik-forward-auth:2.1 \
--config=/conf/traefik-forward-auth.ini
Restart=always
ExecStop=/usr/bin/docker stop -t 2 traefik-forward-auth
[Install]
WantedBy=multi-user.target
Please note the explicit Docker tag of :2.1
to avoid unexpected auto-updates.
In order to start it up, just run:
sudo systemctl daemon-reload
sudo touch /etc/traefik/traefik-forward-auth.env
sudo systemctl start traefik-forward-auth
sudo systemctl enable traefik-forward-auth
Simple auth example
The below new traefik.dynamic.toml
configuration will set up the auth.example.com
endpoint that will be used by Traefik Auth for the Google authentication dance, and set up a middleware that will require users to authenticate whenever visiting wiregate.example.com
[http.routers]
[http.routers.simple]
rule = "Host(`wiregate.example.com`)"
service = "api@internal"
middlewares = ["google-forward-auth"]
[http.routers.simple.tls]
certresolver = "wildcardtls"
[[http.routers.simple.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
[http.routers.auth]
rule = "Host(`auth.example.com`)"
service = "forward-auth"
middlewares = ["google-forward-auth"]
[http.routers.auth.tls]
certresolver = "wildcardtls"
[[http.routers.auth.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
[http.middlewares]
[http.middlewares.google-forward-auth.forwardAuth]
address = "http://127.0.0.1:4181/"
trustForwardHeader = true
authResponseHeaders = ["X-Forwarded-User"]
[http.services]
# Traefik Forward Auth server running locally.
[http.services.forward-auth.loadBalancer]
[[http.services.forward-auth.loadBalancer.servers]]
url = "http://127.0.0.1:4181/"
At this point, if everything went right, you should be able to access the wiregate.example.com
and see a login consent screen from Google.
Setting up WireGuard VPN
Wireguard is a new VPN protocol that is now part of the mainline Linux kernel. Unlike anything else, it is very easy to set up, and automatically resumes in cases of disconnects.
It is idea for our use case: a single Cloud VM that home devices (such as a Rapsberry Pi) can connect to automatically from behind a NAT.
We’ll use 10.212.0.0/24
as the subnet for the private VPN, as it is rarely used by home networks. We will use wiregate
to denote the Cloud VM, and mediaserver
to represent the home device. All IP addresses will be statically configured, and chosen as 10.212.0.1
and 10.212.0.11
respectively.
Note: This set up is different from your typical “I want to browse the internet over VPN”. The home devices will not route their traffic via wiregate
instance, instead this acts as a way for the Traefik proxy to reach services hosted on home devices.
Add GCE firewall entry for Wireguard
Wireguard usually operates over UDP, over the 51820
port.
cloud compute --project=<myproject>
firewall-rules create allow-wireguard --description="Allows wireguard on standard 51820 port." --direction=INGRESS --priority=1000 --network=default --action=ALLOW --rules=udp:51820 --source-ranges=0.0.0.0/0 --target-tags=allow-wireguard
Now edit the VM instance and add a network tag of allow-wireguard
next to https-server
.
Installing Wireguard
On both machines machines install wireguard:
sudo add-apt-repository ppa:wireguard/wireguard
sudo apt install wireguard
We’ll be roughly following the Linode guide with extra info from ArchLinux and the wg-quick
setup from Debian.
Generating secrets
On both machines you need to generate a private and a public key.
umask 077
wg genkey | tee privatekey | wg pubkey > publickey
This will create privatekey
and publickey
files, with obvious content. We will use wiregate_private
, mediaserver_public
etc. to represent the content of these files in subsequent configuration files.
Configure wiregate
side
The wiregate
side of file /etc/wireguard/wg0.conf
:
# Interface for *non internet* VPN setup from remote devices. Each new one needs to added as a peer.
[Interface]
Address = 10.212.0.1/24
ListenPort = 51820
MTU = 1380 # working around GCE MTU limits
PrivateKey = # wiregate_privatekey
[Peer]
# mediaserver
PublicKey = # mediaserver_publickey
AllowedIPs = 10.212.0.11/32
If you want to add any additional peers, just repeate the [Peer]
section with a different IP.
Run:
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
This will make wiregate configure wg0
interface at startup.
Configure mediaserver
side
The mediaserver
side of the file /etc/wireguard/wg0.conf
:
[Interface]
Address = 10.212.0.11/24
MTU = 1380 # same as cloud
PrivateKey = # mediaserver_privatekey
[Peer]
PublicKey = # wiregate_publickey
AllowedIPs = 10.212.0.0/24
Endpoint = wiregate.example.com:51820
PersistentKeepalive = 25
After set up you should see on wiregate
something as follows:
$ sudo wg show
interface: wg0
public key: # wireguard_public_key
private key: (hidden)
listening port: 51820
peer: # mediaserver_public_key
endpoint: 83.115.194.221:22531
allowed ips: 10.212.0.11/32
latest handshake: 51 seconds ago
transfer: 13.91 KiB received, 5.32 KiB sent
This means that the tunnel is working, and you should easily be able to ping 10.212.0.1
and ping 10.212.0.11
from the different sides of the tunnel.
Add HTTP routes
Now that we have a wireguard connection, and Traefik configured with authentication, we can configure an HTTP route. We’ll use Plex Media server as an example.
Add hosts
entries
Add each peer to /etc/hosts
:
# Wireguard hosts
10.212.0.11 mediaserver.vpn.local mediaserver
This will allow you to easily represent hosts in your Traefik configuration.
Traefik rules
We have the on-server interface of Plex Media server running on mediaserver
at its default port 32400
. The final dynamic config file, including the above configuration should look as follows:
[http.routers]
[http.routers.simple]
rule = "Host(`wiregate.example.com`)"
service = "api@internal"
middlewares = ["google-forward-auth"]
[http.routers.simple.tls]
certresolver = "wildcardtls"
[[http.routers.simple.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
[http.routers.auth]
rule = "Host(`auth.example.com`)"
service = "forward-auth"
middlewares = ["google-forward-auth"]
[http.routers.auth.tls]
certresolver = "wildcardtls"
[[http.routers.auth.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
[http.routers.plex]
rule = "Host(`plex.example.com`)"
service = "plex"
middlewares = ["google-forward-auth"]
[http.routers.plex.tls]
certresolver = "wildcardtls"
[[http.routers.plex.tls.domains]]
main = "example.com"
sans = ["*.example.com"]
[http.middlewares]
[http.middlewares.google-forward-auth.forwardAuth]
address = "http://127.0.0.1:4181/"
trustForwardHeader = true
authResponseHeaders = ["X-Forwarded-User"]
[http.services]
# Traefik Forward Auth server running locally.
[http.services.forward-auth.loadBalancer]
[[http.services.forward-auth.loadBalancer.servers]]
url = "http://127.0.0.1:4181/"
# Plex server running on Media server.
[http.services.plex.loadBalancer]
[[http.services.plex.loadBalancer.servers]]
url = "http://mediaserver.vpn.local:32400/"
Note the repetition of domains. If one moved to a YAML file, this would be less repetetive by using YAML anchors.
What’s next
Now that you have a (almost free) Cloud endpoint for your home devices, you can easily build automations that implement web hooks for your home automations.
The primary reason why I set it up was to have a public-facing endpoint for my Home Assistant. The guide on how to set up Home Assistant to work wtih Google Authentication and Google Assistant can be found in a further post.