I could not make sense of this work without research. After three years at CERN, back in Bilbao I decided to work on my own and keep researching and learning. I believe entrepreneurship is the ultimate professional challenge — and, at the same time, an expression of creativity and a claim to freedom.
Zetesis comes from the Greek ζήτησις, inquiry: find the cause before proposing the solution.
In the previous post I described which servers I manage and how I deploy them from git. The network that connects them was left for later: two houses, a tunnel between them and four servers on the internet. This post covers it: the routers, the tunnel, the routes the clusters announce and machine-to-machine access with Tailscale.
As in the previous one, I do not give machine names, addresses or domains.
A VyOS router in each house
Each house has a router running VyOS, a Debian-based router operating system that is configured entirely through a text file. Both houses have used it since June; before that they ran OPNsense.
Both routers do the same job: zone-based firewall, outbound NAT, DHCP, DNS for the home network and their end of the tunnel. The one in the main house, where both clusters live, also speaks BGP with them and filters DNS. The other house keeps the backups, and this post focuses on the main one.
The configuration comes from templates in git
The routers' configuration is not edited on the router. It lives in the infrastructure repository, split into modules by responsibility. The firewall is shared by both routers and each router has its own copy of the other modules:
File
What it holds
10-firewall
The zone-based firewall policy, shared by both routers
20-interfaces
The interfaces and the router's end of the tunnel
30-nat
The house's NAT
40-protocols
Static routes and BGP
50-service
DHCP, DNS, SSH and monitoring
60-system
Identity, users and logging
router.env
The network parameters that are not secret
secrets.env
Keys and passwords, encrypted with SOPS
The modules of a router's configuration.
A script puts the modules together, fills in the parameters and writes a single config.boot per router, which is the file VyOS loads. If a value is missing or left unresolved, it fails before writing anything.
The routers' secrets are encrypted with SOPS
The tunnel keys and the routers' passwords are in git, encrypted with SOPS, a tool that encrypts the values in a file and leaves the field names readable, using age for the encryption. The key that decrypts them is kept in a password manager, outside the clusters.
They are among the few secrets that do not come from Infisical. Infisical runs in the platform cluster, and the routers cannot depend on something that runs inside the network they bring up themselves.
CI checks the templates and the router is updated by hand
Every change to the templates goes through CI, which renders the configuration of both routers with test secrets and checks its structure. It does not validate VyOS syntax; the router does that when it loads the file.
Applying the change is manual: I render the config.boot with the real secrets, load it on the router and confirm it with load, commit and save. Then I check with compare that nothing differs between what is in git and what is running. I never change anything live on the router: if something needs to change, the change goes into the template.
A network for each cluster, apart from the home network
In the main house, the router keeps three networks apart: the home network, with laptops, phones and home automation; the production cluster's network, and the platform cluster's network. The clusters do not share a network with the home devices, so all traffic between them goes through the router, both ways.
The servers have their IP reserved in the router's DHCP, tied to their MAC, and those reservations live in the git templates like the rest of the configuration. Moving a server to another IP is a change that gets reviewed like any other. Both clusters also have a fixed IP on their network.
Home DNS filters ads and malware, and goes out encrypted
Home devices ask the router for DNS. The router caches the answers and passes the rest to unbound, a resolver running in a container on the router itself. Unbound does three things: it blocks the domains on public blocklists for ads, tracking, malware and phishing, with my exceptions in a file in git; it validates the DNSSEC signatures of the answers, and it forwards queries to Quad9 encrypted with DNS over TLS, so the ISP does not see what is being looked up.
The lists download and refresh by themselves. If unbound cannot download one at startup, it resolves without filtering until it gets it, so DNS never goes down because of a list.
A WireGuard tunnel joins the two houses
The two routers are joined by a WireGuard tunnel, a lightweight VPN protocol, that is always up. The houses do not have fixed IPs, so each router finds the other through dynamic DNS. With the tunnel, the networks of both houses see each other as if they were one.
The clusters announce their services over BGP
BGP is the protocol routers use to tell each other which networks each one can reach. Both clusters are in the main house and speak BGP with its router, each with its own autonomous system number. Each cluster reserves the upper half of its network for services that need their own IP, reachable from the home network. When I create one as a LoadBalancer service with a specific label, Cilium, the clusters' network layer, gives it an IP from that range and announces it to the router, which learns it as a single route. I do not have to touch the router for every new service: the cluster announces it and the router learns the route.
So any device on the home network can talk to a service in the clusters, if the router's firewall and Cilium allow it. The clusters also talk to each other over those routes: the production one sends its metrics and logs to the platform one.
Tailscale: every machine with its own identity
For everything else I use Tailscale, a private network between machines built on WireGuard. Each machine joins it with its own identity and a tag, and connections between machines are encrypted, whether they are at home or on the internet. Who can talk to whom is decided by a policy: each access rule says who, where and on which port. Whatever is not in the policy does not get through.
Who
Where
Port
Edge server
Production cluster
80 and 443
VPS and clusters
Backup storage
Just one
Administrator
Every machine
All
Some of the policy's access rules, without names.
The other rules follow the same pattern: one source, one destination and only the port that is needed. Machines without Tailscale, such as the ones on the home network, are reached through subnet routers, which announce that network inside Tailscale.
The policy is in git too
The Tailscale policy is a file in the same repository, with its tests: cases that say which connections must be accepted and which must be rejected. When a change is merged into main, a workflow runs the tests and, if they pass, applies the policy. The routes announced by the subnet routers are also approved from that file, not by hand in the admin console.
The workflow only runs on merges into main, not on pull requests. It needs the Tailscale credential, and on a pull request any branch that touched the file could use it.
Public traffic comes in through the edge server
Public websites do not reach the houses directly. DNS is on Cloudflare, but without its proxy: the records point to the edge server, a VPS running Caddy, a web server, and CrowdSec. Caddy receives the request, CrowdSec blocks addresses with a bad reputation, and the request continues over Tailscale to Traefik, the cluster's entry point, which hands it to the service.
How I get around LaLiga's blocks
LaLiga, Spain's football league, has a court authorization, valid until the end of the 2026/27 season, for Movistar, MasOrange, Vodafone and DIGI to block during matches the Cloudflare IPs it associates with pirate streams. They block the whole IP, and thousands of websites share those IPs. When the pirate site goes down, all the others go down with it, without warning.
I noticed on the day the league started, in August: two of my websites would not open from a phone, while the ones served directly from the VPS worked. Finding the cause took me hours, because from the fibre at home, on an ISP that was not applying the block, everything answered fine.
The block is applied in the visitor's ISP network, not in the data centre: from the VPS, Cloudflare answers without trouble. So the edge server faces the visitor and Cloudflare stays behind it. Static websites are still published on Cloudflare Pages, and Caddy forwards requests to them: deployment does not change and only the VPS sees the blockable IP.
For that, DNS points to the VPS with an A record without proxy. A proxied CNAME would resolve to Cloudflare's IPs even if the target were Pages. Caddy gets the HTTPS certificate from Let's Encrypt with a DNS challenge, which works without the proxy, and it doubles as a check: if the website shows the Let's Encrypt certificate rather than Cloudflare's, traffic no longer goes through its proxy.
When the website runs on a machine of mine with its own IP, such as one of the client forums, Caddy runs on that same machine and the record points to it: going through the edge server would only add a hop. The rule is the same, a Caddy of mine with an unproxied record faces the visitor. The exception is whatever sits behind Cloudflare Access, because there the proxy is what asks for the login, and I accept it may not open during a match.
Publishing a new website means adding its block to the Caddy file in the repository, which the server's agent applies on merge, creating the unproxied DNS record and adding it to the probes. The probes check every domain and alert me on Telegram if one has been down for three minutes. They run from home, so they catch service failures but not another ISP's block; for that I check hayahora.futbol.
The network is reviewed in git too
The routers, with their IP reservations and their DNS, the Tailscale policy and the services the clusters announce live in the same repository as the rest of the infrastructure. A network change is a commit that can be reviewed beforehand and looked up afterwards. More detail on every piece, with the machines' names, is in Building a homelab from scratch: networking.