~ ~ whoami whoami projects projects blog blog

What Your Home Network Does When You're Not Looking

A year ago I published a roadmap. Everything on it shipped. This is what that looks like.

The Roadmap That Actually Shipped

A year ago I wrote a post about my home network infrastructure. It ended with a section called "What's Next": a tidy bullet list of things I intended to build. Monitoring. DNS filtering. A VPN. The kind of list that, in most tech blogs, ages like milk.

This is not most tech blogs.

Every item on that list shipped. Not as a weekend hack, not as a proof of concept left running on a Raspberry Pi with a sticky note that says "DO NOT REBOOT". Shipped as declarative NixOS configuration, version-controlled in git, deployed via Colmena, with secrets encrypted by sops-nix. The full stack. The entire cult ritual.

The original post described a network where Rogal Dorn ran four services and Ferrus Manus was described as "planned". Today, Rogal Dorn runs over twenty services behind a reverse proxy with forward authentication. Ferrus Manus handles DNS for every device on the network. A VPS in Germany tunnels traffic from the public internet to my living room. And a monitoring stack watches all of it, around the clock, reporting to my phone when something goes wrong.

This post is about what that looks like in practice, and why it matters beyond the satisfaction of checking items off a list.

The DNS Layer, or How to See What Your Network Actually Does

Your ISP gives you a router. The router handles DNS. You never think about it. This is by design.

DNS is the first question every device on your network asks: "Where is this thing I want to talk to?" Your smart TV asks it. Your phone asks it. Your child's tablet asks it 400 times before breakfast. And the answer comes from whoever controls your DNS. By default, that is your ISP, a company whose business model does not include telling you what your devices are doing.

Pi-hole runs on Ferrus Manus, a Raspberry Pi 5. It is the default gateway and DNS server for every device on the network. One line in the Nix configuration points every device's DNS at the Pi instead of the ISP router. That is all it takes to change who answers the first question.

The revelation is not the ad blocking, though that is a pleasant side effect. The revelation is the query log. Within the first hour of running Pi-hole, I watched a Samsung television make 300 DNS requests to tracking domains. A "smart" plug phoned home to a server in Shenzhen every 90 seconds. The gaming console made more DNS requests in standby mode than my laptop does during a working day.

None of this is visible from your ISP router's admin panel. None of it is visible from the devices themselves. It is visible from the thing that answers their first question.

Control the DNS, control the first answer every device receives. Block the domains you do not trust. Log the ones you want to understand. This is not surveillance for the sake of it. This is the baseline act of knowing what happens on a network you pay for, in a house you own, on devices you bought.

The fact that this requires a dedicated computer running custom software is itself a statement about what consumer networking equipment is designed to hide from you.

The VPN, or Taking Your Network With You

Hotel WiFi is a social contract where you agree to let strangers see your traffic in exchange for the privilege of watching a buffering YouTube video at 240p. Public WiFi at a coffee shop is the same contract, but the coffee costs more.

WireGuard runs via Pangolin on Lotara, a Hetzner VPS in Germany. The VPN endpoint lives in the cloud because home IP addresses change and ISPs have opinions about inbound traffic. The VPS is the stable front door. One of the reasons for this architecture is precisely that it obfuscates the home IP address: the public internet sees the VPS, not my router.

The interesting part is not the tunnel itself. WireGuard is fast, lightweight, and well-documented. The interesting part is what happens inside the tunnel. The client configuration on Saul (the laptop) sets the DNS server to Pi-hole at home and routes all traffic through the VPN with AllowedIPs = 0.0.0.0/0.

Even when connected from a hotel in another country, DNS queries resolve through Pi-hole. The ad blocking follows you. The tracking protection follows you. The query log follows you. Your network's policies travel with you, because the VPN is not just an encrypted pipe: it is a policy enforcement mechanism.

The hotel sees one encrypted WireGuard connection. Nothing else.

NordVPN promises privacy. Mine delivers it, because I can read the configuration file. There is no trust involved. No terms of service. No marketing copy about "military-grade encryption" that means nothing. Just a WireGuard peer definition and a DNS server I control.

The Captive Portal Problem

Hotels and airports use captive portals: you connect to WiFi, and before you can reach anything, you must click through a login page. This does not work when all your traffic routes through a VPN that the captive portal cannot reach.

Saul has a captive portal script built into NixOS. Run captive-portal open and it stops WireGuard, applies a restrictive iptables firewall that allows only DHCP, DNS, HTTP, HTTPS, and the WireGuard endpoint. Authenticate with the portal. Run captive-portal close and WireGuard comes back up, the NixOS firewall restores, and all traffic routes through the tunnel again.

The restrictive firewall during portal mode is the key detail. Default policy: DROP on INPUT, FORWARD, and OUTPUT. Only the minimum traffic needed to authenticate with the portal passes through. The laptop is exposed to the hostile network for thirty seconds, with the smallest possible attack surface.

This is a NixOS module. Declarative. Reproducible. Version-controlled. Not a bash script I wrote at 2am in a Holiday Inn and then lost.

Twenty-Two Doors, One Bouncer

Caddy runs on Rogal Dorn as a reverse proxy, terminating TLS for over twenty subdomains under cleencode.dev. Cloudflare DNS challenge handles certificate issuance. No ports exposed to the internet besides 80 and 443. Every service gets its own subdomain, its own certificate, its own access policy.

Authelia sits behind Caddy as a forward authentication provider. The default policy is deny.

access_control = {
  default_policy = "deny";
  rules = [
    # Only specific domains get bypass, one_factor, or two_factor
    # Everything else is denied by default
  ];
};

Deny everything. Then write rules for the things you trust. This is the opposite of how consumer routers work, where everything is permitted by default and you are supposed to notice when something goes wrong.

Services that support it use OIDC for single sign-on through Authelia. Eight services authenticate this way: Immich, Mealie, Gitea, Jellyfin, Home Assistant, Paperless, Nextcloud, and Grafana. One identity provider. One login. One audit trail.

Services that do not support OIDC use Caddy's forward_auth directive. Before the request reaches the service, Caddy asks Authelia: "Is this user allowed?" If not, Authelia redirects to a login page. The service never sees the unauthenticated request. It does not need to know about authentication at all.

forward_auth [server]:9091 {
  uri /api/verify?rd=https://auth.[domain]
  copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}

The societal angle here is mundane and important. One misconfigured port and your family photos are public. One service without authentication and your recipe collection becomes an entry point to your network. The default should always be denial. The effort should be in granting access, not restricting it. Consumer hardware gets this backwards because convenience sells better than security.

Watching the Watchers

The monitoring stack lives on Rogal Dorn, not on Ferrus Manus as originally planned. The Pi 5 handles DNS admirably, but Prometheus and Grafana want more RAM than 8GB allows when you are also running a DNS server with a million blocked domains.

The Stack

What Gets Probed

The blackbox exporter probes 25 endpoints. Seventeen internal HTTP services, from Jellyfin to Paperless to the homepage dashboard. Eight external HTTPS endpoints, including the public-facing subdomains and the main website. Every probe checks for a valid HTTP response. The HTTPS probes also monitor SSL certificate expiry.

Lotara's node exporter is scraped remotely over HTTPS with basic auth, so the VPS's system metrics appear alongside the home server's. One Grafana dashboard. Two physical locations. Complete visibility.

The Alert Rules

Eight alert rules fire when things go wrong:

All alerts route to Home Assistant via webhook. Home Assistant sends push notifications to my phone. I know my internet speed has degraded before I notice the buffering. I have historical data before I ring my ISP. I can say "my download speed dropped below 50 Mbps at 14:32 on Tuesday and has not recovered" instead of "the internet feels slow".

The ISP support script does not have a response for that. They are used to "have you tried turning it off and on again", not timestamped Prometheus metrics.

Deny Everything, Then Start Asking Questions

NixOS ships with a stateful firewall. It is enabled by default but permissive by default, which is a contradiction that tells you everything about the state of consumer network security.

Every host in this network declares its firewall rules explicitly:

# Saul (laptop) - deny all inbound
networking.firewall = {
  enable = true;
  allowedTCPPorts = [ ];
  allowedUDPPorts = [ ];
};

# Rogal Dorn - only open what services need
networking.firewall.allowedTCPPorts = [
  # HTTP, HTTPS, and only the ports
  # that running services require.
  # Nothing else.
];

These rules live in git. They are reviewed in pull requests. They are tested before deployment. They are reproducible across rebuilds. If a machine dies and I rebuild it from configuration, the firewall rules come back identical. No manual iptables commands. No "I think I opened port 8080 six months ago but I cannot remember why."

Consumer routers default to allowing all outbound traffic. The logic is that threats come from outside. This was arguably true in 2005. Today, the interesting threats are outbound. A compromised smart device does not wait for inbound connections. It phones home. It joins a botnet. It exfiltrates data. And your router's default configuration lets it, because blocking outbound traffic would break things, and breaking things generates support calls, and support calls cost money.

The captive portal script on Saul demonstrates the alternative. When exposed to a hostile network, the default OUTPUT policy is DROP. Only DHCP, DNS, HTTP/HTTPS, and the WireGuard endpoint are permitted. This is what "deny by default" looks like for outbound traffic: you enumerate what is allowed and drop everything else.

It is more work. It occasionally breaks things. But when something breaks, you know exactly which rule to examine, because the rules are written down.

The Network Map

The original post had a network diagram with "planned" next to half the components. Here is the updated version, with nothing planned and everything running:


                    Internet
                       |
                 ISP Router
                       |
          +------------+------------+
          |            |            |
    Ferrus Manus   Rogal Dorn     Magnus
    Raspberry Pi   Lenovo P53     Dell OptiPlex
    [DNS/Pi-hole]  [Services]     [NAS/Storage]
                       |
              +--------+--------+
              |                 |
           Caddy          Monitoring
           Authelia       Prometheus
           22 services    Grafana
           8 OIDC         Alertmanager
                          NetAlertX
              |
              | (Pangolin Tunnel)
              |
           Lotara
           Hetzner VPS
           [WireGuard/Website]
        

Ferrus Manus answers DNS for the network. Every device asks it first. Rogal Dorn runs the services, the authentication layer, and the monitoring stack. Magnus stores the data: media, backups, documents. Lotara faces the internet: the website you are reading, the WireGuard endpoint, the Pangolin tunnel that connects public subdomains to home services.

Lotara currently serves double duty as both the public website host and the tunnel endpoint. Splitting those into separate VPS instances is on the list: a compromised tunnel should not give you the website, and vice versa. Separation of concerns applies to infrastructure as much as it does to code.

Four machines at home, one in the cloud. Each with a clear responsibility. Each declared in Nix. Each deployable from a single git repository with colmena apply.

The Cost of Caring

I wrote a post last year about surrendering to the panopticon. About the futility of individual privacy in a world of corporate surveillance. The same author who wrote that post built the system described in this one. The irony is deliberate.

An important distinction: this system is built to prevent surveillance, not to perform it. I do not monitor what other people on my network are doing. I do not read their DNS queries. I do not inspect their traffic. The Pi-hole blocks ads and trackers for everyone equally. The monitoring stack watches services and infrastructure, not users. The point is to stop corporations from surveilling the household, not to replace them with a more local version of the same behaviour.

The difference is who holds the data. Google knows your DNS queries because your ISP router forwards them to 8.8.8.8 by default. Your smart TV manufacturer knows your viewing habits because the TV phones home and nobody told you. Your ISP knows every domain you visit because DNS is unencrypted and they can see it.

In this system, the data stays on hardware I own. The DNS logs are on a Raspberry Pi in my office. The monitoring metrics are on a laptop repurposed as a server. The VPN configuration is a text file in a git repository. Nobody monetises the DNS queries. Nobody correlates viewing habits with an advertising profile. Nobody sells access to my network traffic patterns.

This is not a principled stand against surveillance capitalism. It is a practical acknowledgement that if someone is going to have infrastructure-level visibility into my home network, it should be the person who lives there and maintains it.

The Honest Costs

Time. Hundreds of hours of NixOS configuration, debugging, and learning. Weeks of reading documentation for tools that assume you already know what you are doing.

Complexity. This system has moving parts. Prometheus scrapes exporters. Alertmanager evaluates rules. Grafana renders dashboards. Caddy terminates TLS. Authelia checks credentials. Each component can fail. Each failure requires diagnosis. Each diagnosis requires understanding how the components interact.

Maintenance. Software updates. Certificate renewals (automated, but I still check). Alert rule tuning. Dashboard adjustments. The monitoring system itself requires monitoring, which is either recursive absurdity or good engineering practice depending on your perspective.

Electricity. Four machines running continuously. The Pi draws 5 watts. The laptop server draws 30. The NAS draws 40. The VPS draws whatever Hetzner charges for. It adds up. Not enormously, but it adds up.

The Societal Point

The fact that understanding your own network requires this level of effort is itself the problem. Not everyone can spend hundreds of hours learning NixOS. Not everyone has spare hardware for a monitoring stack. Not everyone can read a WireGuard configuration file. But everyone deserves to know what their devices are doing, what data leaves their home, and who has access to their network.

The tools exist. The knowledge is publicly available. The barrier is time and complexity, not secrecy. And that barrier exists because the companies selling you routers, smart devices, and "cloud" services have no incentive to lower it. Opacity is the product. Your ignorance of your own network is a feature, not a bug.

This setup is not a template. It is not a tutorial. It is evidence that a different relationship with your home network is possible, if you are willing to do the work. And a quiet argument that the work should not be necessary.

The Emperor protects. Your firewall should too.