The Goal
I have a DigitalOcean Kubernetes cluster running services like n8n (workflow automation), SearXNG (search), PostgreSQL, and Redis. I also have a Raspberry Pi 5 running OpenWrt as my home router. I wanted every device on my WiFi – laptops, phones, tablets – to be able to access my K8s services directly by ClusterIP, without installing Tailscale (or any VPN client) on each device.
The result: my wife can open http://10.245.15.122:5678 on her laptop and hit n8n. My iPad can reach SearXNG. Any device on the WiFi just… works. The OpenWrt router handles the encrypted tunnel transparently.
The Architecture
WiFi Device (192.168.1.x)
→ OpenWrt Router (192.168.1.1)
→ Tailscale tunnel (encrypted WireGuard)
→ K8s Subnet Router pod (10.245.0.0/16)
→ K8s Service (e.g., n8n at 10.245.15.122:5678)
Two Tailscale subnet routers create a site-to-site mesh:
- OpenWrt advertises
192.168.1.0/24(home WiFi LAN) - K8s pod advertises
10.245.0.0/16(Kubernetes ClusterIP range)
Both accept each other’s routes. The magic is in the OpenWrt firewall config that lets WiFi clients route through the Tailscale tunnel without knowing it exists.
Cost: $0. Tailscale’s free tier supports subnet routing, and devices behind a subnet router don’t count toward the 100-device limit.
Prerequisites
- An OpenWrt router (I’m using a Raspberry Pi 5 with OpenWrt 25.12.2)
- A Kubernetes cluster (I’m using DigitalOcean, but any cluster works)
- A Tailscale account (free tier is fine)
- About 30 minutes
Part 1: Tailscale on OpenWrt
Installation
OpenWrt 25.x uses apk (not opkg):
apk add tailscale
(1/2) Installing kmod-tun (6.12.74-r1)
(2/2) Installing tailscale (1.94.1-r1)
Enable and start:
/etc/init.d/tailscale enable
/etc/init.d/tailscale start
Authentication
tailscale up --accept-dns=false --advertise-routes=192.168.1.0/24
--accept-dns=false: Don’t let Tailscale override your DNS config (you want dnsmasq to keep handling DNS)--advertise-routes=192.168.1.0/24: Tell the tailnet about your WiFi LAN
This prints an authentication URL. Open it in a browser, log in to Tailscale, and authorize the device.
Then enable accepting routes from other subnet routers:
tailscale set --accept-routes=true
Approve the Route
Go to Tailscale Admin Console, click on the openwrt machine, and approve the 192.168.1.0/24 subnet route.
Part 2: Tailscale Subnet Router in Kubernetes
Deploy a Tailscale pod that advertises your K8s ClusterIP range. I use Terraform, but you can adapt this to plain YAML.
The Terraform Config
variable "tailscale_auth_key" {
type = string
sensitive = true
}
resource "kubernetes_secret" "tailscale_auth" {
metadata {
name = "tailscale-auth"
namespace = "default"
}
data = {
TS_AUTHKEY = var.tailscale_auth_key
}
}
resource "kubernetes_service_account" "tailscale" {
metadata {
name = "tailscale"
namespace = "default"
}
}
resource "kubernetes_role" "tailscale" {
metadata {
name = "tailscale"
namespace = "default"
}
rule {
api_groups = [""]
resources = ["secrets"]
verbs = ["create", "get", "update", "patch"]
}
}
resource "kubernetes_role_binding" "tailscale" {
metadata {
name = "tailscale"
namespace = "default"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "Role"
name = kubernetes_role.tailscale.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.tailscale.metadata[0].name
namespace = "default"
}
}
resource "kubernetes_deployment" "tailscale" {
metadata {
name = "tailscale-subnet-router"
labels = { app = "tailscale" }
}
spec {
replicas = 1
selector {
match_labels = { app = "tailscale" }
}
template {
metadata {
labels = { app = "tailscale" }
}
spec {
service_account_name = kubernetes_service_account.tailscale.metadata[0].name
container {
name = "tailscale"
image = "ghcr.io/tailscale/tailscale:latest"
env {
name = "TS_AUTHKEY"
value_from {
secret_key_ref {
name = kubernetes_secret.tailscale_auth.metadata[0].name
key = "TS_AUTHKEY"
}
}
}
env { name = "TS_USERSPACE"; value = "true" }
env { name = "TS_ROUTES"; value = "10.245.0.0/16" }
env { name = "TS_HOSTNAME"; value = "k8s-subnet-router" }
env { name = "TS_KUBE_SECRET"; value = "tailscale-state" }
env { name = "TS_ACCEPT_DNS"; value = "false" }
resources {
requests = { memory = "64Mi", cpu = "50m" }
limits = { memory = "128Mi" }
}
}
}
}
}
}
The key environment variable is TS_ROUTES = "10.245.0.0/16" – this tells Tailscale to advertise the entire Kubernetes ClusterIP range.
Generate an Auth Key
Go to Tailscale Admin > Settings > Keys and create a reusable auth key. Store it as a Kubernetes secret or (as I do) a GitHub Actions secret for Terraform.
Approve the K8s Route
After the pod starts, go to the Tailscale Admin Console, click on k8s-subnet-router, and approve the 10.245.0.0/16 subnet route.
Part 3: The OpenWrt Firewall Config (The Hard Part)
This is where it gets interesting. At this point:
- The Pi can reach K8s via Tailscale (the route is in table 52)
- But WiFi clients have no idea
10.245.x.xtraffic should go to the Pi
We need to:
- Tell OpenWrt about the Tailscale interface
- Create a firewall zone that allows traffic to flow between WiFi and Tailscale
- Enable masquerading so return traffic finds its way back
Create the Tailscale Network Interface
uci set network.tailscale=interface
uci set network.tailscale.proto="none"
uci set network.tailscale.device="tailscale0"
uci commit network
Create the Firewall Zone
uci add firewall zone
uci set firewall.@zone[-1].name="tailscale"
uci set firewall.@zone[-1].input="ACCEPT"
uci set firewall.@zone[-1].output="ACCEPT"
uci set firewall.@zone[-1].forward="ACCEPT"
uci set firewall.@zone[-1].masq="1"
uci set firewall.@zone[-1].network="tailscale"
The masq="1" is critical – it enables NAT (masquerading) on the Tailscale interface. Without it, K8s services receive packets with a 192.168.1.x source IP, but they don’t know how to route replies back to your WiFi network.
Create Forwarding Rules
# WiFi LAN → Tailscale (outbound to K8s)
uci add firewall forwarding
uci set firewall.@forwarding[-1].src="lan"
uci set firewall.@forwarding[-1].dest="tailscale"
# Tailscale → WiFi LAN (return traffic)
uci add firewall forwarding
uci set firewall.@forwarding[-1].src="tailscale"
uci set firewall.@forwarding[-1].dest="lan"
uci commit firewall
Apply Everything
/etc/init.d/network restart
/etc/init.d/firewall restart
And here’s where things went sideways.
The Bugs We Hit
Bug 1: Firewall Restart Killed Tailscale Connectivity
After restarting the firewall, the Pi couldn’t ping the K8s subnet router anymore:
ping -c 2 100.108.47.63
# 2 packets transmitted, 0 packets received, 100% packet loss
Tailscale has its own nftables chains (ts-input, ts-forward, ts-postrouting) that manage tunnel traffic. When OpenWrt’s firewall restarts, it rebuilds the nftables ruleset and Tailscale’s rules get disrupted.
Fix: Restart Tailscale after the firewall restart.
/etc/init.d/tailscale restart
After the restart, connectivity came back:
ping -c 2 100.108.47.63
# 64 bytes from 100.108.47.63: seq=0 ttl=64 time=90.892 ms
Bug 2: “Operation not permitted” When Accessing K8s Services
Even though pings worked to the Tailscale IP, accessing K8s ClusterIPs failed:
wget -q -O /dev/null --spider http://10.245.15.122:5678/
# Failed to send request: Operation not permitted
The K8s subnet routes weren’t appearing in the kernel routing table. Running ip route showed nothing for 10.245.0.0/16. But Tailscale manages routes in a separate routing table (table 52):
ip route show table 52
10.245.0.0/16 dev tailscale0
100.97.117.88 dev tailscale0
100.100.100.100 dev tailscale0
100.108.47.63 dev tailscale0
The routes were there – they just weren’t in the main table. After restarting Tailscale (which fixed the nftables chains), the routing worked because Tailscale’s policy routing rules in ip rule list direct traffic through table 52.
Bug 3: The Route Disappeared After Network Restart
This is related to Bug 1. The sequence matters:
- Configure network and firewall via
uci - Restart network:
/etc/init.d/network restart - Restart firewall:
/etc/init.d/firewall restart - Restart Tailscale:
/etc/init.d/tailscale restart← Don’t forget this!
If you skip step 4, Tailscale’s nftables rules are stale and routing breaks. Always restart Tailscale last.
The Final Test
From a Mac on the WiFi network (192.168.1.182), with no Tailscale client installed:
# n8n workflow automation
curl -s -o /dev/null -w "HTTP %{http_code}" http://10.245.15.122:5678/
# HTTP 200
# SearXNG search engine
curl -s -o /dev/null -w "HTTP %{http_code}" http://10.245.67.150:8080/
# HTTP 200
# PostgreSQL
nc -z -w 3 10.245.20.103 5432
# Connection succeeded!
# Redis
nc -z -w 3 10.245.116.226 6379
# Connection succeeded!
Every K8s service, reachable from any device on the WiFi, through an encrypted WireGuard tunnel, with zero client configuration.
Is This Allowed by Tailscale’s Terms?
Yes. This is an officially supported use case. From Tailscale’s documentation:
“In cases where you can’t install Tailscale on every device on your physical network, you can set up a subnet router to access these devices from your tailnet.”
Subnet routing is available on the free tier. Devices behind a subnet router don’t count toward your device limit. Site-to-site networking is a documented, intended feature.
The Full Firewall Configuration
For reference, here’s the complete OpenWrt firewall state after configuration:
Zones:
lan - input:ACCEPT output:ACCEPT forward:ACCEPT network:lan
wan - input:REJECT output:ACCEPT forward:DROP network:wan,wan6 masq:1
tailscale - input:ACCEPT output:ACCEPT forward:ACCEPT network:tailscale masq:1
Forwarding:
lan → wan (WiFi clients reach the internet)
lan → tailscale (WiFi clients reach K8s)
tailscale → lan (K8s return traffic reaches WiFi clients)
What You Get
| Path | How It Works |
|---|---|
| WiFi device → K8s service | Traffic goes through OpenWrt’s Tailscale tunnel automatically |
| Phone on cellular → K8s service | Install Tailscale on your phone, access via ClusterIP from anywhere |
| Laptop at coffee shop → K8s service | Install Tailscale on the laptop, full access via ClusterIP |
The subnet routing through OpenWrt handles the home WiFi case. Individual Tailscale installs handle the remote access case. Both paths coexist.
Cost
| Component | Monthly Cost |
|---|---|
| Tailscale (free tier, 3 users, 100 devices) | $0 |
| Tailscale pod in K8s (~64MB RAM) | $0 (runs on existing nodes) |
| OpenWrt on Raspberry Pi 5 | $0 (already your router) |
| Total | $0 |
This was configured live in a terminal session with Claude Code. The bugs were real – especially the Tailscale restart ordering issue. If you’re reading this because your subnet routing isn’t working after a firewall change, restart Tailscale. Trust me.