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
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.
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
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
gcloud beta compute --project=<myproject> instances create wiregate --zone=us-east1-c --machine-type=f1-micro --subnet=default --address=126.96.36.199 --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
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
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
/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 = "firstname.lastname@example.org" # change me storage = "acme.json" [certificatesResolvers.simpletls.acme.tlsChallenge]
Dynamic configuration file
/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
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.
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_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 = "email@example.com" # changeme storage = "/conf/acme.json" [certificatesResolvers.wildcardtls.acme.dnsChallenge] resolvers = ["188.8.131.52:53", "184.108.40.206: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.
/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
[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.
As with any OAuth2 flow, we need to tell the upstream authentication provider who we are.
We will use a file0based configuration placing it in
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
[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
[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.
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
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
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
On both machines machines install wireguard:
sudo add-apt-repository ppa:wireguard/wireguard sudo apt install wireguard
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
publickey files, with obvious content. We will use
mediaserver_public etc. to represent the content of these files in subsequent configuration files.
wiregate side of file
# 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.
sudo systemctl enable wg-quick@wg0 sudo systemctl start wg-quick@wg0
This will make wiregate configure
wg0 interface at startup.
mediaserver side of the file
[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: 220.127.116.11: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 each peer to
# Wireguard hosts 10.212.0.11 mediaserver.vpn.local mediaserver
This will allow you to easily represent hosts in your Traefik configuration.
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.
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.