graph TB
subgraph Internet
EXTUSER[External Users]
CF[Cloudflare Zero Trust]
end
subgraph homelan[Home LAN 192.168.88.0/24]
USER[LAN Clients]
RT[MikroTik Router .1]
CF_RT[cloudflared RouterOS container 192.168.202.2]
subgraph pve1[pve1 .100]
CP1[talos-cp1 .246 - Control Plane]
W1[talos-worker1 .249]
W2[talos-worker2 .248]
W3[talos-worker3 .247]
end
subgraph pve2[pve2 .101]
CP2[talos-cp2 .241 - Control Plane]
W4[talos-worker4 .243]
CLAW_VM[clawdbot .103]
TNAS[TrueNAS .230]
end
subgraph pve3[pve3 .102]
CP3[talos-cp3 .242 - Control Plane]
W5[talos-worker5 .244]
W6[talos-worker6 .240]
end
subgraph nas[Synology DS920plus .19]
NFS_SVC[NFS Server]
DOCKER[Docker Apps]
MON_EXP[Exporters]
end
LB[Istio Gateway VIP .12 - MetalLB L2]
end
EXTUSER --> CF
USER -->|HTTPS| LB
CF -->|single tunnel: homelab-k8s| CF_RT
CF_RT -->|routes 7 public hostnames| LB
RT --> LB
LB --> CP1
CP1 ---|raft| CP2
CP2 ---|raft| CP3
CP1 --- W1
CP1 --- W2
CP1 --- W3
CP2 --- W4
CP3 --- W5
CP3 --- W6
NFS_SVC -.->|NFS v4| CP1
TNAS -.->|NVMe NFS| CP1
MON_EXP -.->|metrics scrape| CP1
style LB fill:#f96,stroke:#333
style CP1 fill:#69f,stroke:#333
style CP2 fill:#69f,stroke:#333
style CP3 fill:#69f,stroke:#333
style CF fill:#f90,stroke:#333
style CF_RT fill:#f90,stroke:#333
style NFS_SVC fill:#6c6,stroke:#333
A single Cloudflare tunnel homelab-k8s runs as a container on the MikroTik router (app-cloudflared, RouterOS container IP 192.168.202.2). It terminates the tunnel locally and forwards each public hostname to the Istio gateway with a Host-header rewrite — so external traffic and LAN traffic land on the same Istio HTTPRoute / public TLS cert pair.
| Public Domain |
Tunnel rewrites Host to |
Istio HTTPRoute |
wiki.vyanh.uk |
wikijs.homelab.vyanh.uk |
nas-ingress/wikijs |
ntfy.vyanh.uk |
(passthrough) |
direct to 192.168.88.12 for ntfy.vyanh.uk |
vault.vyanh.uk |
vaultwarden.homelab.vyanh.uk |
nas-ingress/vaultwarden |
tracker.vyanh.uk |
lifeops.homelab.vyanh.uk |
life-ops/lifeops-tracker-public |
auth.vyanh.uk |
auth.homelab.vyanh.uk |
ente-auth/museum |
accounts.vyanh.uk |
accounts.homelab.vyanh.uk |
ente-auth/accounts |
dns.vyanh.uk |
(passthrough) |
direct to 192.168.88.12 for DoH |
tasks.vyanh.uk |
vikunja.homelab.vyanh.uk |
vikunja/vikunja |
Moving cloudflared to MikroTik means the tunnel stays up even during full K8s cluster outages, and removes K8s cluster resource consumption. There is no longer a separate tunnel inside the Vaultwarden compose stack — vault.vyanh.uk rides the same homelab-k8s tunnel as everything else.
| IP Address |
Hostname |
Role |
Notes |
| 192.168.88.1 |
MikroTik |
Router / Firewall / DHCP |
NAT, DNS forwarding to Technitium |
| 192.168.88.12 |
Istio Gateway VIP |
K8s Ingress (MetalLB L2) |
All *.homelab.vyanh.uk traffic |
| 192.168.88.19 |
Synology NAS |
DS920+ Storage + Docker |
NFS, Immich, Vaultwarden, etc. |
| 192.168.88.100 |
pve1 |
Proxmox hypervisor |
talos-cp1 + worker1/2/3 |
| 192.168.88.101 |
pve2 |
Proxmox hypervisor |
talos-cp2 + worker4, clawdbot, TrueNAS |
| 192.168.88.102 |
pve3 |
Proxmox hypervisor |
talos-cp3 + worker5/6 |
| 192.168.88.103 |
clawdbot |
VM on pve2 |
Telegram bot + Ollama |
| 192.168.88.230 |
TrueNAS-Scale |
Secondary NAS (VM on pve2) |
NVMe NFS for K8s metrics/logs storage |
| 192.168.88.240 |
talos-worker6 |
K8s Worker (pve3) |
Workloads |
| 192.168.88.241 |
talos-cp2 |
K8s Control Plane (pve2) |
API server, etcd, scheduler |
| 192.168.88.242 |
talos-cp3 |
K8s Control Plane (pve3) |
API server, etcd, scheduler |
| 192.168.88.243 |
talos-worker4 |
K8s Worker (pve2) |
Workloads |
| 192.168.88.244 |
talos-worker5 |
K8s Worker (pve3) |
Workloads |
| 192.168.88.246 |
talos-cp1 |
K8s Control Plane (pve1) |
API server, etcd, scheduler |
| 192.168.88.247 |
talos-worker3 |
K8s Worker (pve1) |
Workloads |
| 192.168.88.248 |
talos-worker2 |
K8s Worker (pve1) |
Workloads |
| 192.168.88.249 |
talos-worker1 |
K8s Worker (pve1) |
Workloads |
graph TD
CLIENT[LAN Client] -->|DNS Query| RT[MikroTik Router]
RT -->|Forward to Technitium| TC[Technitium 3-node HA in K8s]
TC -->|Blocked domain| BLOCK[Return NXDOMAIN]
TC -->|homelab.vyanh.uk| LOCAL[Return 192.168.88.12 - Istio Gateway VIP]
TC -->|External query| DOH[Upstream DoH - Cloudflare and Google]
DOH -->|Answer| TC
TC -->|Answer| CLIENT
K8S_POD[K8s Pod] -->|DNS Query| CDNS[CoreDNS]
CDNS -->|cluster.local| K8S_SVC[K8s Service DNS]
CDNS -->|wildcard homelab.vyanh.uk| CLUSTERIP[Return 10.102.56.238 - Istio gateway ClusterIP]
CDNS -->|External| TC2[Technitium ClusterIPs - 10.99.188.56 and 10.105.215.168 and 10.107.50.50]
EDNS[ExternalDNS] -->|Watch HTTPRoute or Gateway| ING[Gateway API Resources]
EDNS -->|rfc2136 TSIG| TC
style BLOCK fill:#f66,stroke:#333
style LOCAL fill:#6f6,stroke:#333
style CLUSTERIP fill:#6f6,stroke:#333
Key Points:
- Technitium runs as 3 Deployments in the
technitium namespace (primary, secondary, tertiary) for HA
- CoreDNS forwards to Technitium via ClusterIP services (not MetalLB LB IPs) — pods cannot reach MetalLB VIPs due to hairpin NAT
- For in-cluster pod DNS,
*.homelab.vyanh.uk returns the Istio gateway ClusterIP (10.102.56.238), not the MetalLB VIP — same hairpin reason
- ExternalDNS watches
HTTPRoute resources and auto-creates Technitium DNS records via rfc2136 + TSIG
- Technitium resolves upstream using DoH (DNS-over-HTTPS) to Cloudflare and Google
sequenceDiagram
participant Client
participant MetalLB as MetalLB (L2 .12)
participant Istio as Istio Gateway
participant CrowdSec as CrowdSec EnvoyFilter
participant CM as cert-manager
participant LE as Let's Encrypt
participant CF as Cloudflare DNS
participant App as Backend Service
Client->>MetalLB: HTTPS *.homelab.vyanh.uk
MetalLB->>Istio: Forward to VIP 192.168.88.12
Note over Istio,CM: First request or cert renewal
Istio->>CM: TLS cert needed
CM->>LE: DNS-01 ACME challenge
LE->>CF: Verify _acme-challenge TXT record
CF-->>LE: Verified
LE-->>CM: Certificate issued (wildcard)
CM-->>Istio: TLS secret stored in istio-system
Istio->>Istio: TLS termination (TLS 1.3 min)
Istio->>CrowdSec: ext_authz check (IP reputation + AppSec WAF)
CrowdSec-->>Istio: Allow / Block
Istio->>App: HTTP to ClusterIP via HTTPRoute
App-->>Istio: Response
Istio-->>Client: HTTPS response
Configuration Details:
- MetalLB IP pool:
192.168.88.10–192.168.88.30 (Layer 2 advertisement)
- Istio Gateway (
homelab-gateway in istio-system): TLS 1.3 minimum, externalTrafficPolicy: Local
- CrowdSec attaches as an Envoy
ext_authz filter on the gateway — every request passes through IP reputation + OWASP AppSec WAF
- cert-manager issuers:
letsencrypt-dns01 (primary, wildcard via Cloudflare DNS-01) + letsencrypt-prod (fallback HTTP-01)
graph LR
DEV[Developer] -->|git push| GL[GitLab - gitlab.homelab.vyanh.uk]
subgraph Pipeline[GitLab CI Pipeline - k8s-runner in cluster]
DETECT[Detect Changes] --> LINT[Lint and Test]
LINT --> GATE[CI Gate]
GATE --> BUILD[Kaniko Build in-cluster]
BUILD --> SIGN[Cosign Sign by digest]
SIGN --> SCAN[Trivy Scan HIGH plus CRITICAL]
SCAN --> UPDATE[Update Manifests image tag]
end
GL --> DETECT
BUILD -->|push image| HARBOR[Harbor Registry - harbor.homelab.vyanh.uk]
UPDATE -->|push tag| K8SREPO[k8s-cluster-config GitLab repo]
K8SREPO -->|detect drift| ARGO[ArgoCD auto-sync]
ARGO -->|sync| K8S[Kubernetes Cluster]
K8S -->|pull image| HARBOR
style GATE fill:#ff9,stroke:#333
style ARGO fill:#f96,stroke:#333
style HARBOR fill:#69f,stroke:#333
Flow Details:
- Developer pushes to
LifeOps repo on self-hosted GitLab
- GitLab CI runs 6-stage pipeline on in-cluster K8s runner (
k8s-runner tag, gitlab-runner namespace)
- Kaniko builds container images in-cluster (no Docker daemon needed)
- Images pushed to Harbor (
harbor.homelab.vyanh.uk/applications/)
- Cosign signs images by digest with private key
- Trivy scans for HIGH/CRITICAL vulnerabilities + generates SBOM
- Pipeline updates image tags in k8s-cluster-config GitLab repo (via
K8S_CONFIG_PAT)
- ArgoCD detects the change and syncs to cluster
- Pods pull new images from Harbor
graph TD
TF[Terraform CLI - MinIO S3 backend] -->|Create policies and roles| VAULT[HashiCorp Vault - 3-node HA Raft]
TF -->|Seed placeholders| VAULT
ADMIN[Admin] -->|Set real values via UI or CLI| VAULT
VAULT -->|K8s Auth JWT| VSO[Vault Secrets Operator vso-system]
VSO -->|Watch VaultStaticSecret CRs| K8S_API[K8s API]
VSO -->|Create or Update every 60s| SECRET[K8s Secrets in app namespace]
SECRET -->|Mount as env or volume| POD[Application Pods]
subgraph PerNS[Per-Namespace Resources in k8s-cluster-config]
SA[ServiceAccount vso-auth]
VA[VaultAuth CR - role vso-namespace]
VSS[VaultStaticSecret CR - path kv/namespace]
SA --> VA
VA --> VSS
end
VSO --> SA
Key Properties:
- Per-namespace isolation: each namespace has its own Vault policy scoped to
kv/data/<namespace>/*
- Automatic refresh: VSO syncs secrets every 60 seconds (configurable per VaultStaticSecret)
- Placeholder convention: all Terraform seeds use
"PLACEHOLDER_UPDATE_IN_VAULT" for easy grep
- OIDC dual-path pattern: OIDC client secrets stored at both
authentik/<app>-oidc AND <app-namespace>/oidc
- VSO targets
vault-active.vault.svc:8200 (not the generic service) to avoid routing to a standby node that returns 500
- Terraform state: stored in MinIO (
192.168.88.19:9000, bucket terraform-state, key k8s-infra-lab/terraform.tfstate) since 2026-04-28 — migrated off Terraform Cloud, no more TF_API_TOKEN CI variable
For deep-dive component workflows, see Cluster Components & Workflows.