Skip to content

Router-Hosts Integration Design

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Deploy router-hosts-operator v0.7.0 to manage K8s service DNS entries via CRDs, and use terraform-provider-routerhosts for infrastructure hosts, replacing the manually-maintained /extdata/hosts/main file on Firewalla.

Architecture: Three-component system where Ansible manages the router-hosts gRPC server on Firewalla, ArgoCD deploys the Kubernetes operator for service DNS entries, and Terraform manages infrastructure host entries. All components communicate via mTLS-secured gRPC.

Tech Stack: router-hosts v0.7.0, router-hosts-operator Helm chart, terraform-provider-routerhosts, Vault PKI, ArgoCD, HCP Terraform


┌─────────────────────────────────────────────────────────────────────────┐
│ Firewalla Router │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ router-hosts server v0.7.0 │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ gRPC API │────│ SQLite3 │────│ hosts file │ │ │
│ │ │ (mTLS) │ │ Database │ │ /hosts/main │ │ │
│ │ └─────────────┘ └──────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ dnsmasq hostsdir │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ dnsmasq │ │
│ │ hostsdir=/extdata/router-hosts/hosts │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
▲ ▲
│ gRPC (mTLS) │ gRPC (mTLS)
│ │
┌───────────────────┴───────────┐ ┌──────────┴──────────────────────────┐
│ Kubernetes Cluster │ │ HCP Terraform │
│ ┌─────────────────────────┐ │ │ ┌──────────────────────────────┐ │
│ │ router-hosts-operator │ │ │ │ terraform-provider- │ │
│ │ ┌───────────────────┐ │ │ │ │ routerhosts │ │
│ │ │ HostEntry CRDs │ │ │ │ │ ┌────────────────────────┐ │ │
│ │ │ (K8s services) │ │ │ │ │ │ routerhosts_entry │ │ │
│ │ └───────────────────┘ │ │ │ │ │ (infra hosts) │ │ │
│ └─────────────────────────┘ │ │ │ └────────────────────────┘ │ │
└───────────────────────────────┘ │ └──────────────────────────────┘ │
└─────────────────────────────────────┘

Responsibility Split:

ComponentManagesExamples
K8s OperatorServices on MetalLB IPsvault, grafana, argocd, auth, longhorn, windmill, mealie
TerraformInfrastructure hostsTPI nodes (tpi-alpha-, tpi-beta-), NAS (nas-*), API VIP

Upgrade the Ansible-deployed router-hosts server from v0.6.0 to v0.7.0.

Changes to ansible/roles/router-hosts/defaults/main.yml

Section titled “Changes to ansible/roles/router-hosts/defaults/main.yml”
# Version upgrade
router_hosts_version: "v0.7.0" # was v0.6.0

Changes to ansible/roles/router-hosts/templates/server.toml.j2

Section titled “Changes to ansible/roles/router-hosts/templates/server.toml.j2”
[server]
bind_address = "{{ router_hosts_bind_address }}:{{ router_hosts_grpc_port }}"
hosts_file_path = "/hosts/main" # was /data/hosts
[database]
path = "/data/router-hosts.sqlite3" # was /data/router-hosts.db
[tls]
cert_file = "/certs/tls.crt"
key_file = "/certs/tls.key"
ca_file = "/certs/ca.crt"

Add new volume mount for hosts directory:

volumes:
- {{ router_hosts_data_dir }}:/data
- {{ router_hosts_hosts_dir }}:/hosts # NEW
- {{ router_hosts_certs_dir }}:/certs:ro
router_hosts_hosts_dir: "{{ router_hosts_base_dir }}/hosts"

Deploy via ArgoCD using the Helm chart from ghcr.io/fzymgc-house/router-hosts-operator.

argocd/
└── apps/
└── router-hosts-operator/
├── kustomization.yaml
├── namespace.yaml
├── application.yaml
├── external-secret.yaml
└── values.yaml

argocd/apps/router-hosts-operator/namespace.yaml

Section titled “argocd/apps/router-hosts-operator/namespace.yaml”
apiVersion: v1
kind: Namespace
metadata:
name: router-hosts-operator

argocd/apps/router-hosts-operator/external-secret.yaml

Section titled “argocd/apps/router-hosts-operator/external-secret.yaml”
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: router-hosts-mtls
namespace: router-hosts-operator
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: router-hosts-mtls
template:
type: kubernetes.io/tls
data:
tls.crt: "{{ .cert }}"
tls.key: "{{ .private_key }}"
ca.crt: "{{ .issuing_ca }}"
dataFrom:
- extract:
key: pki/issue/router-hosts-client
conversionStrategy: Default
rewrite:
- regexp:
source: "certificate"
target: "cert"

argocd/apps/router-hosts-operator/values.yaml

Section titled “argocd/apps/router-hosts-operator/values.yaml”
operator:
routerHostsServer:
address: "192.168.20.1:50051"
tls:
enabled: true
secretName: router-hosts-mtls

argocd/apps/router-hosts-operator/application.yaml

Section titled “argocd/apps/router-hosts-operator/application.yaml”
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: router-hosts-operator
namespace: argocd
spec:
project: default
source:
repoURL: ghcr.io/fzymgc-house/router-hosts-operator
targetRevision: 0.7.0
chart: router-hosts-operator
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: router-hosts-operator
syncPolicy:
automated:
prune: true
selfHeal: true
apiVersion: routerhosts.fzymgc.house/v1alpha1
kind: HostEntry
metadata:
name: vault
namespace: vault
spec:
hostname: vault
ip: 192.168.20.145

New HCP Terraform workspace for infrastructure host management.

tf/
└── router-hosts/
├── versions.tf
├── terraform.tf
├── variables.tf
├── outputs.tf
├── main.tf
└── hosts.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
routerhosts = {
source = "fzymgc-house/routerhosts"
version = "~> 0.7.0"
}
vault = {
source = "hashicorp/vault"
version = "~> 4.0"
}
}
}
terraform {
cloud {
organization = "fzymgc-house"
workspaces {
name = "main-cluster-router-hosts"
}
}
}
variable "router_hosts_address" {
description = "Address of the router-hosts gRPC server"
type = string
default = "192.168.20.1:50051"
}
# Get mTLS certificates from Vault PKI
data "vault_pki_secret_backend_cert" "router_hosts" {
backend = "pki"
name = "router-hosts-client"
common_name = "terraform-router-hosts"
ttl = "24h"
}
provider "routerhosts" {
address = var.router_hosts_address
tls {
cert = data.vault_pki_secret_backend_cert.router_hosts.certificate
key = data.vault_pki_secret_backend_cert.router_hosts.private_key
ca_cert = data.vault_pki_secret_backend_cert.router_hosts.ca_chain
}
}
# TuringPi Alpha Board
resource "routerhosts_entry" "tpi_alpha_1" {
hostname = "tpi-alpha-1"
ip = "192.168.20.141"
}
resource "routerhosts_entry" "tpi_alpha_2" {
hostname = "tpi-alpha-2"
ip = "192.168.20.142"
}
resource "routerhosts_entry" "tpi_alpha_3" {
hostname = "tpi-alpha-3"
ip = "192.168.20.143"
}
resource "routerhosts_entry" "tpi_alpha_4" {
hostname = "tpi-alpha-4"
ip = "192.168.20.144"
}
# TuringPi Beta Board
resource "routerhosts_entry" "tpi_beta_1" {
hostname = "tpi-beta-1"
ip = "192.168.20.151"
}
resource "routerhosts_entry" "tpi_beta_2" {
hostname = "tpi-beta-2"
ip = "192.168.20.152"
}
resource "routerhosts_entry" "tpi_beta_3" {
hostname = "tpi-beta-3"
ip = "192.168.20.153"
}
resource "routerhosts_entry" "tpi_beta_4" {
hostname = "tpi-beta-4"
ip = "192.168.20.154"
}
# Kubernetes API VIP
resource "routerhosts_entry" "k8s_api" {
hostname = "k8s-api"
ip = "192.168.20.140"
}
# NAS Systems
resource "routerhosts_entry" "nas_alpha" {
hostname = "nas-alpha"
ip = "192.168.20.200"
}
resource "routerhosts_entry" "nas_alpha_iscsi" {
hostname = "nas-alpha-iscsi"
ip = "192.168.20.201"
}
resource "routerhosts_entry" "nas_alpha_replication" {
hostname = "nas-alpha-replication"
ip = "192.168.20.202"
}
resource "routerhosts_entry" "nas_beta" {
hostname = "nas-beta"
ip = "192.168.20.210"
}

Terminal window
brew install fzymgc-house/tap/router-hosts

Bash/Zsh:

Terminal window
# Create config directory and request certificates from Vault
mkdir -p ~/.config/router-hosts && \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-$(whoami)" ttl=720h | \
jq -r '.data.certificate' > ~/.config/router-hosts/tls.crt && \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-$(whoami)" ttl=720h | \
jq -r '.data.private_key' > ~/.config/router-hosts/tls.key && \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-$(whoami)" ttl=720h | \
jq -r '.data.issuing_ca' > ~/.config/router-hosts/ca.crt && \
cat > ~/.config/router-hosts/config.toml << 'EOF'
[server]
address = "192.168.20.1:50051"
[tls]
cert_file = "~/.config/router-hosts/tls.crt"
key_file = "~/.config/router-hosts/tls.key"
ca_file = "~/.config/router-hosts/ca.crt"
EOF

Fish:

Terminal window
# Create config directory and request certificates from Vault
mkdir -p ~/.config/router-hosts; and \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-"(whoami) ttl=720h | \
jq -r '.data.certificate' > ~/.config/router-hosts/tls.crt; and \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-"(whoami) ttl=720h | \
jq -r '.data.private_key' > ~/.config/router-hosts/tls.key; and \
vault write -format=json pki/issue/router-hosts-client \
common_name="cli-"(whoami) ttl=720h | \
jq -r '.data.issuing_ca' > ~/.config/router-hosts/ca.crt; and \
cat > ~/.config/router-hosts/config.toml << 'EOF'
[server]
address = "192.168.20.1:50051"
[tls]
cert_file = "~/.config/router-hosts/tls.crt"
key_file = "~/.config/router-hosts/tls.key"
ca_file = "~/.config/router-hosts/ca.crt"
EOF
Terminal window
# List all hosts
router-hosts list
# Get specific host
router-hosts get vault
# Add test entry
router-hosts add test-entry 192.168.20.99
# Remove test entry
router-hosts delete test-entry

  1. Upgrade router-hosts server to v0.7.0 via Ansible
  2. Configure directory separation (hosts vs database)
  3. Verify server connectivity with CLI
  1. Create tf/router-hosts workspace
  2. Import existing infrastructure hosts as Terraform resources
  3. Verify entries via CLI
  1. Deploy operator via ArgoCD
  2. Create HostEntry CRDs for each K8s service
  3. Verify entries appear in router-hosts database
  1. Update Firewalla dnsmasq configuration to point to new hosts directory
  2. Remove manual /extdata/hosts/main file
  3. Verify DNS resolution for all hosts

  • dnsmasq config: ~/.firewalla/config/dnsmasq_local/local-hosts.conf
  • Points to: /extdata/hosts (manual file)
  • router-hosts server writes to: /extdata/router-hosts/data/hosts
  • dnsmasq config updated to point to router-hosts managed directory
  • Separate hosts output from database storage
/extdata/router-hosts/
├── data/
│ └── router-hosts.sqlite3 # SQLite3 database
├── hosts/
│ └── main # Generated hosts file (dnsmasq reads this)
└── certs/
├── tls.crt
├── tls.key
└── ca.crt

Ansible Template: templates/local-hosts.conf.j2

Section titled “Ansible Template: templates/local-hosts.conf.j2”
hostsdir={{ router_hosts_hosts_dir }}
expand-hosts
domain=fzymgc.house
- name: Configure dnsmasq to use router-hosts managed directory
ansible.builtin.template:
src: local-hosts.conf.j2
dest: ~/.firewalla/config/dnsmasq_local/local-hosts.conf
mode: '0644'
notify: Restart dnsmasq
  • No database migration required (current DB is test data only)
  • v0.7.0 uses SQLite3 as default database backend
  • Directory separation ensures dnsmasq only reads the hosts file, not the database

Files:

  • Modify: ansible/roles/router-hosts/defaults/main.yml
  • Modify: ansible/roles/router-hosts/templates/server.toml.j2
  • Modify: ansible/roles/router-hosts/templates/docker-compose.yml.j2
  • Create: ansible/roles/router-hosts/templates/local-hosts.conf.j2
  • Modify: ansible/roles/router-hosts/tasks/main.yml

Files:

  • Create: tf/router-hosts/versions.tf
  • Create: tf/router-hosts/terraform.tf
  • Create: tf/router-hosts/variables.tf
  • Create: tf/router-hosts/outputs.tf
  • Create: tf/router-hosts/main.tf
  • Create: tf/router-hosts/hosts.tf

Files:

  • Create: argocd/apps/router-hosts-operator/kustomization.yaml
  • Create: argocd/apps/router-hosts-operator/namespace.yaml
  • Create: argocd/apps/router-hosts-operator/application.yaml
  • Create: argocd/apps/router-hosts-operator/external-secret.yaml
  • Create: argocd/apps/router-hosts-operator/values.yaml

Task 4: Create HostEntry CRDs for K8s Services

Section titled “Task 4: Create HostEntry CRDs for K8s Services”

Files:

  • Create: argocd/apps/*/hostentry.yaml for each service namespace

Files:

  • Create: docs/operations/router-hosts.md
  1. Run Ansible playbook to upgrade server
  2. Apply Terraform to create infrastructure hosts
  3. Sync ArgoCD to deploy operator
  4. Apply HostEntry CRDs
  5. Update dnsmasq configuration
  6. Verify DNS resolution