This page documents every major subsystem in the cluster and how they wire together. For the high-level network topology and node IPs see Architecture & Network Topology.
9 Talos Linux nodes across 3 Proxmox hypervisors. Control plane is HA (3 nodes, Raft etcd quorum requires 2/3).
graph TB
subgraph pve1[pve1 - 192.168.88.100]
CP1[talos-cp1 .246 - Control Plane]
W1[talos-worker1 .249]
W2[talos-worker2 .248]
W3[talos-worker3 .247]
end
subgraph pve2[pve2 - 192.168.88.101]
CP2[talos-cp2 .241 - Control Plane]
W4[talos-worker4 .243]
end
subgraph pve3[pve3 - 192.168.88.102]
CP3[talos-cp3 .242 - Control Plane]
W5[talos-worker5 .244]
W6[talos-worker6 .240]
end
CP1 ---|etcd raft| CP2
CP2 ---|etcd raft| CP3
CP1 ---|pod network| W1
CP1 ---|pod network| W2
CP1 ---|pod network| W3
CP2 ---|pod network| W4
CP3 ---|pod network| W5
CP3 ---|pod network| W6
style CP1 fill:#69f,stroke:#333
style CP2 fill:#69f,stroke:#333
style CP3 fill:#69f,stroke:#333
K8s version: Talos v1.12.6 running Kubernetes v1.35.4, containerd 2.1.6.
kube-vip provides a stable VIP for the K8s API server — all nodes use it for control-plane HA. MetalLB (wave 1) provides the Istio ingress VIP on 192.168.88.12.
Cilium replaces kube-proxy and Flannel entirely using eBPF. Every pod-to-pod path is controlled by CiliumNetworkPolicy (CNP) resources stored in k8s-cluster-config.
graph TB
subgraph Node[Worker Node any]
POD_A[Pod A - app namespace]
POD_B[Pod B - other namespace]
KUBELET[kubelet - health probes]
subgraph Cilium[Cilium eBPF dataplane]
BPF_IN[Ingress BPF hook - identity check]
BPF_EG[Egress BPF hook - identity check]
HUBBLE[Hubble observer - flow logging]
end
end
POD_A -->|egress| BPF_EG
BPF_EG -->|CNP: fromEndpoints check| POD_B
KUBELET -->|reserved:remote-node - identity 6| BPF_IN
BPF_IN -->|CNP: fromEntities host+remote-node| POD_A
BPF_EG -.->|log| HUBBLE
BPF_IN -.->|log| HUBBLE
style Cilium fill:#e8f4f8,stroke:#4a9aba
style HUBBLE fill:#c8e6c9,stroke:#388e3c
Critical cluster-specific behaviour: With enable-endpoint-routes: true + native routing + kube-proxy-replacement: true, kubelet health probes from remote nodes arrive with Cilium identity reserved:remote-node (ID 6), not reserved:host (ID 1). Every CNP with a probe rule must include both:
- fromEntities:
- host
- remote-node
toPorts:
- ports:
- port: "<probe-port>"
protocol: TCP
Using only host silently drops cross-node probes → liveness failures → crash-loops.
Hubble provides real-time flow observability at hubble.homelab.vyanh.uk. Use it to diagnose Policy denied DROPPED drops.
A short tuning pass enabled three eBPF / kernel features to take advantage of the Talos 1.12 kernel and the bursty homelab traffic profile:
| Feature |
Cilium / kernel setting |
Why it helps here |
| BBR congestion control |
bpf-tcp-congestion: bbr |
TCP throughput holds up better under a single-uplink / small-buffer topology than cubic; particularly noticeable on Longhorn replication and Velero S3 PUTs to MinIO. |
| BIG TCP |
enable-ipv4-big-tcp: true (kernel ≥ 5.19) |
GSO/GRO past the 64K boundary lets long-lived NFS / Longhorn flows ship larger SKBs in one syscall, reducing per-packet CPU on the worker nodes. |
| Endpoint routes |
enable-endpoint-routes: true + routing-mode: native + kube-proxy-replacement: true |
Removes the cilium_host veth hop on intra-node traffic; pod-to-pod inside one node stays in eBPF. Side effect: kubelet probes now arrive as reserved:remote-node (see CNP rule above). |
A complementary Istio change reduced control-plane chatter by slowing the config push debounce (PILOT_DEBOUNCE_AFTER=200ms, PILOT_DEBOUNCE_MAX=2s) — istiod was previously re-pushing on every individual HTTPRoute reconcile during ArgoCD batch syncs, churning Envoy across every gateway pod.
The combined effect was a measurable drop in NAS NFS latency p99 and a noticeable cut in istiod CPU during ArgoCD bulk syncs. None of these are correctness changes — if you suspect a regression, flip them off one at a time in the cilium Helm values and re-test.
Every external request passes through this stack before reaching an application pod:
sequenceDiagram
participant C as Client LAN/CF tunnel
participant MLB as MetalLB L2 .12
participant IGW as Istio Gateway - homelab-gateway
participant CS as CrowdSec - Envoy ext_authz
participant APPSEC as CrowdSec AppSec - OWASP CRS WAF
participant APP as App Pod - via HTTPRoute
C->>MLB: TCP SYN → 192.168.88.12:443
MLB->>IGW: Forward (L2 ARP, externalTrafficPolicy=Local)
IGW->>IGW: TLS 1.3 termination - wildcard cert from cert-manager
IGW->>CS: ext_authz: is source IP blocked?
CS->>APPSEC: Deep request inspection (WAF)
APPSEC-->>CS: Allow / Block (fail-open on timeout)
CS-->>IGW: 200 Allow / 403 Block
IGW->>APP: HTTP (plain) via HTTPRoute → ClusterIP Service
APP-->>IGW: Response
IGW-->>C: HTTPS response
HTTPRoute matching — each app has an HTTPRoute in its namespace pointing at its Service. ExternalDNS reads the HTTPRoute.spec.hostnames and creates a DNS A record in Technitium automatically.
CrowdSec operates in two modes simultaneously:
- LAPI (Deployment): aggregates decisions, pulls community blocklist from CAPI every 1h
- Agent (DaemonSet): parses Istio gateway access logs (nginx-format) from the gateway pods, feeds local signals to LAPI
- AppSec (port 7422): per-request OWASP CRS WAF — fail-open (traffic passes if AppSec is unreachable)
Three layers of DNS resolution work together:
graph TB
subgraph LAN[LAN DNS client-facing]
MIKROTIK[MikroTik DHCP - adverts .88.1 as DNS]
MIKROTIK -->|forward *.homelab.vyanh.uk - + all external| TC_P
end
subgraph K8s_DNS[K8s Technitium namespace - technitium]
TC_P[Primary - ClusterIP 10.99.188.56]
TC_S[Secondary - ClusterIP 10.105.215.168]
TC_T[Tertiary - ClusterIP 10.107.50.50]
TC_P -.->|zone transfer| TC_S
TC_P -.->|zone transfer| TC_T
end
subgraph CoreDNS[CoreDNS kube-system]
CD[CoreDNS - 3-replica Deployment]
CD -->|*.homelab.vyanh.uk - -> 10.102.56.238 - Istio ClusterIP| ISTIO_SVC[Istio Gateway - ClusterIP Service]
CD -->|cluster.local| K8S_SVC[K8s Service DNS]
CD -->|external queries| TC_P
end
subgraph ExternalDNS[ExternalDNS wave 7]
EDNS[ExternalDNS - watches HTTPRoute]
EDNS -->|rfc2136 TSIG update| TC_P
end
TC_P -->|DoH| UPSTREAM[Cloudflare 1.1.1.1 - Google 8.8.8.8]
style TC_P fill:#f96,stroke:#333
style CD fill:#69f,stroke:#333
Why two different IPs for *.homelab.vyanh.uk?
- LAN clients → Technitium returns
192.168.88.12 (MetalLB VIP — reachable from physical network)
- K8s pods → CoreDNS returns
10.102.56.238 (Istio gateway ClusterIP — pods cannot use MetalLB VIP due to hairpin NAT)
Technitium features in use: DNS-over-HTTPS upstream, ad/tracker blocklist, DNSSEC validation, RFC2136 dynamic updates (for ExternalDNS), zone transfer (primary→secondary→tertiary sync).
cert-manager (wave 0) runs before everything else and issues all TLS certificates.
graph LR
subgraph cert-manager[cert-manager wave 0]
CTRL[Controller - watches Certificate CRs]
WEBHOOK[Webhook - validates CRs]
end
subgraph Issuers[ClusterIssuers]
DNS01[letsencrypt-dns01 - primary wildcard - DNS-01 via Cloudflare API]
HTTP01[letsencrypt-prod - fallback - HTTP-01]
end
subgraph Vault_PKI[Internal PKI]
VAULT_ISSUER[Vault PKI issuer - vault-internal-tls]
VAULT_CA[Vault CA - self-signed root]
end
CTRL --> DNS01
CTRL --> HTTP01
CTRL --> VAULT_ISSUER
VAULT_ISSUER --> VAULT_CA
DNS01 -->|*.homelab.vyanh.uk| WILDCARD_CERT[Wildcard TLS Secret - istio-system homelab-tls]
HTTP01 -->|individual hostnames| EXTRA_CERTS[Extra certs]
VAULT_CA -->|mTLS| VAULT_INTERNAL[Vault inter-node TLS - vault-internal-tls]
WILDCARD_CERT --> ISTIO[Istio Gateway - termination]
style DNS01 fill:#6f9,stroke:#333
style WILDCARD_CERT fill:#ff9,stroke:#333
The wildcard cert *.homelab.vyanh.uk covers all internal services from a single cert/renewal cycle. cert-manager stores the Cloudflare API token as a K8s secret (populated by VSO from Vault).
All secrets flow through Vault → VSO → K8s Secrets. No hardcoded credentials anywhere in Git.
graph TB
subgraph TF[Terraform Cloud bootstrap]
TF_VAULT[Creates Vault policies - per-namespace read roles - seeds PLACEHOLDER values]
end
subgraph VAULT[HashiCorp Vault wave 4 3-node HA]
RAFT[Raft storage - 3 replicas across pve1 2 3]
TRANSIT[vault-transit on NAS - auto-unseal key]
KV[kv-v2 engine - kv <namespace> <secret>]
end
subgraph VSO[Vault Secrets Operator wave 5]
VSO_CTRL[Controller - polls every 60s]
VAULT_AUTH[VaultAuth CRs - 1 per namespace]
end
subgraph NS[Each App Namespace]
SA[ServiceAccount - vso-auth]
VSS[VaultStaticSecret CR - points at kv path]
K8S_SECRET[K8s Secret - auto-created updated]
end
ADMIN[Admin] -->|vault kv put kv/app/secret - real values| KV
TF_VAULT --> VAULT
TRANSIT -->|seal/unseal| RAFT
KV --> VSO_CTRL
VSO_CTRL -->|K8s auth JWT| VAULT_AUTH
VAULT_AUTH --> SA
VSS --> VSO_CTRL
VSO_CTRL -->|create/update| K8S_SECRET
K8S_SECRET -->|env/volume mount| PODS[App Pods]
style VAULT fill:#e8f0fe,stroke:#4285f4
style VSO fill:#fce8b2,stroke:#f9ab00
style K8S_SECRET fill:#e6f4ea,stroke:#34a853
Vault HA gotcha: VSO targets vault-active.vault.svc:8200 (not the generic vault.vault.svc) to avoid being routed to a standby that returns HTTP 500.
Vault auto-unseal: Uses a vault-transit container on the NAS (192.168.88.19:8201). If the NAS is down, Vault pods start sealed and VSO cannot sync secrets until it's reachable.
graph TB
subgraph Dev[Developer Workflow]
DEV[Developer]
LIFEOPS_REPO[LifeOps GitLab repo - gitlab.homelab.vyanh.uk]
K8S_REPO[k8s-cluster-config - GitLab repo]
BOOTSTRAP[argocd-bootstrap - GitLab repo]
end
subgraph CI[GitLab CI Pipeline k8s-runner]
DETECT[1. Detect Changes - which service changed?]
LINT[2. Lint and Test - go test eslint]
GATE[3. CI Gate - must pass to continue]
KANIKO[4. Kaniko Build - in-cluster no Docker daemon]
SIGN[5. Cosign Sign - by image digest]
TRIVY[6. Trivy Scan - HIGHandCRITICAL gate]
MANIFEST[7. Update Manifests - git push new image tag]
end
subgraph GitOps[GitOps ArgoCD]
ROOT_APP[Root App - argocd root-app.yaml]
CHILD_APPS[Child Applications - one per core-component]
CLUSTER[K8s Cluster]
HARBOR[Harbor Registry - harbor.homelab.vyanh.uk]
end
DEV -->|git push| LIFEOPS_REPO
LIFEOPS_REPO --> DETECT
DETECT --> LINT --> GATE --> KANIKO
KANIKO -->|push image| HARBOR
KANIKO --> SIGN --> TRIVY --> MANIFEST
MANIFEST -->|git push via K8S_CONFIG_PAT| K8S_REPO
K8S_REPO -->|poll every 3 min| ROOT_APP
ROOT_APP --> CHILD_APPS
CHILD_APPS -->|sync| CLUSTER
CLUSTER -->|pull image| HARBOR
BOOTSTRAP -->|kustomize apply| ROOT_APP
style GATE fill:#ff9,stroke:#333
style HARBOR fill:#69f,stroke:#333
style ROOT_APP fill:#f96,stroke:#333
ArgoCD app-of-apps pattern: root-app.yaml recursively scans core-components/**/application.yaml. Every subdirectory with an application.yaml becomes a child ArgoCD Application. Sync waves control the order.
Helm multi-source pattern used by every component:
- Source 1: upstream Helm chart repo (e.g.
charts.cilium.io)
- Source 2: Git ref
$values → core-components/<name>/values.yaml
- Source 3 (optional): Git path →
core-components/<name>/resources/ (VSO manifests, CNPs, etc.)
Three storage tiers with distinct trade-offs:
graph TB
subgraph Tier1[Tier 1 - Longhorn K8s node SSDs]
LH[Longhorn Controller]
LH_VOL[Replicated block volumes - 2-3 replicas across nodes - StorageClass - longhorn]
LH --> LH_VOL
end
subgraph Tier2[Tier 2 - TrueNAS NVMe NFS .230]
TNAS_NFS[TrueNAS Scale NFS - NVMe SSD pool]
SC_RETAIN[nfs-subdir-retain - ReclaimPolicy - Retain]
SC_DELETE[nfs-subdir-delete - ReclaimPolicy - Delete]
TNAS_NFS --> SC_RETAIN
TNAS_NFS --> SC_DELETE
end
subgraph Tier3[Tier 3 - Synology HDD NFS .19]
SYN_NFS[Synology DS920and NFS - 4×8TB WD HDD]
SC_SYN[nfs-synology - ReclaimPolicy - Retain - uid gid 1024 required]
SYN_NFS --> SC_SYN
end
subgraph Workloads[Workload Assignment]
DB[Databases - Postgres Redis - → Longhorn]
METRICS[Metrics and Logs - VictoriaMetrics VictoriaLogs - → TrueNAS Retain]
CI[CI Runner workspaces - → TrueNAS Delete]
FILES[User Files - Nextcloud data - → Synology]
end
LH_VOL --> DB
SC_RETAIN --> METRICS
SC_DELETE --> CI
SC_SYN --> FILES
style Tier1 fill:#e8f4f8,stroke:#4a9aba
style Tier2 fill:#f3e5f5,stroke:#7b1fa2
style Tier3 fill:#e8f5e9,stroke:#2e7d32
Synology NFS gotcha: Synology root_squash maps root to uid 1024. All pods writing to NFS Synology volumes must runAsUser: 1024, runAsGroup: 1024. Set initChownData.enabled: false — chown fails with root_squash.
| StorageClass |
Backend |
Media |
Use For |
ReclaimPolicy |
longhorn |
K8s node SSDs |
NVMe/SSD replicated |
Databases, Vault, critical stateful |
Retain |
nfs-subdir-retain |
TrueNAS .230 |
NVMe SSD |
Metrics, logs, traces, RWX blobs |
Retain |
nfs-subdir-delete |
TrueNAS .230 |
NVMe SSD |
Ephemeral CI workspaces |
Delete |
nfs-synology |
Synology .19 |
4×8TB HDD |
Nextcloud files, large bulk storage |
Retain |
graph TB
subgraph Sources[Metric Sources]
K8S_NODES[K8s Nodes - kubelet cAdvisor]
KSM[kube-state-metrics - pod PVC deploy health]
NE_K8S[Node Exporters - per K8s node DaemonSet]
NE_NAS[Node Exporter - Synology NAS]
NE_PVE[PVE Exporter - Proxmox hypervisors]
SNMP[SNMP Exporters - MikroTik and Synology SNMP]
end
subgraph LogSources[Log Sources]
POD_LOGS[Pod stdout - all namespaces]
RT_SYSLOG[MikroTik Syslog - UDP 514]
end
subgraph Pipeline[Collection Pipeline]
VMA[vmagent DaemonSet - scrapes and remote_write]
VECTOR[Vector DaemonSet - tail pod logs]
OTEL[OpenTelemetry Collector - OTLP receiver]
end
subgraph Storage[Storage nfs-subdir-retain]
VM[VictoriaMetrics - 90d retention 15Gi]
VL[VictoriaLogs - 30d retention 15Gi]
TEMPO[Grafana Tempo - 14d in MinIO]
end
subgraph Alerting[Alerting]
VMALERT[vmalert - 30s eval interval]
AM[Alertmanager]
NTFY_BRIDGE[ntfy bridge -9099 - Python reformatter]
NTFY[ntfy - homelab-alerts topic]
end
subgraph Viz[Visualization]
GRAFANA[Grafana - Authentik OIDC SSO]
end
K8S_NODES --> VMA
KSM --> VMA
NE_K8S --> VMA
NE_NAS --> VMA
NE_PVE --> VMA
SNMP --> VMA
VMA -->|remote_write| VM
POD_LOGS --> VECTOR
RT_SYSLOG --> VECTOR
VECTOR -->|HTTP insert| VL
OTEL -->|remote_write| VM
OTEL -->|OTLP| TEMPO
VM --> VMALERT
VMALERT --> AM
AM --> NTFY_BRIDGE
NTFY_BRIDGE --> NTFY
VM --> GRAFANA
VL --> GRAFANA
TEMPO --> GRAFANA
style VM fill:#f96,stroke:#333
style VL fill:#69f,stroke:#333
style GRAFANA fill:#6c6,stroke:#333
style NTFY fill:#f60,stroke:#333
Alert routing topics:
homelab-alerts — K8s/vmalert alerts
homelab-security — CrowdSec / Vault-watcher / ismartgate
homelab-ops — Watchtower / MikroTik
homelab-ups — UPS events from Synology
graph LR
subgraph Schedule[Velero Schedule]
SCHED[daily-full - 0 1 * * * - All namespaces except kube-system]
end
subgraph Velero[Velero wave 6]
VEL_CTRL[Velero Controller]
KOPIA[Kopia fs-backup - per-namespace repository]
end
subgraph BackupRepo[BackupRepository per namespace]
REPO[kopia <namespace> - in velero-backups bucket]
end
subgraph MinIO[MinIO on NAS .19 -9000]
BUCKET[velero-backups bucket - user - velero Velero2024x]
end
subgraph Restore[Restore Flow]
VEL_RESTORE[velero restore create - --from-backup <name>]
NS_RESTORE[Namespace and PVCs - recreated from snapshot]
end
SCHED --> VEL_CTRL
VEL_CTRL -->|1. Snapshot K8s objects - Deployments Services ConfigMaps Secrets| SNAPSHOT[K8s manifests tarball]
VEL_CTRL -->|2. fs-backup PVC data - via Kopia| KOPIA
KOPIA -->|deduplicated blocks| REPO
REPO --> BUCKET
SNAPSHOT --> BUCKET
VEL_RESTORE --> VEL_CTRL
BUCKET -->|restore| NS_RESTORE
style BUCKET fill:#f96,stroke:#333
style KOPIA fill:#69f,stroke:#333
One Kopia repository per namespace — Velero creates a BackupRepository object per namespace on first backup. If stale data exists in the MinIO path without a proper Kopia manifest (kopia.repository), Velero will refuse to initialize (found existing data in storage location). Fix: delete the stale MinIO prefix, then kubectl delete backuprepository <name> -n velero.
Authentik (wave 10) is the single identity provider for all web-facing services.
sequenceDiagram
participant User
participant App as App e.g. Grafana, Harbor
participant Istio as Istio Gateway
participant Authentik as Authentik - authentik.homelab.vyanh.uk
participant VAULT as Vault - OIDC client secret
User->>Istio: GET https://grafana.homelab.vyanh.uk
Istio->>App: Forward request
App->>User: 302 Redirect to Authentik OIDC /authorize
User->>Authentik: Login (username + password + 2FA)
Authentik->>Authentik: Validate credentials
Authentik->>User: 302 Redirect back with auth code
User->>App: Auth code
App->>Authentik: Exchange code for token (using OIDC client secret)
Note over App,VAULT: OIDC client secret injected by VSO - from Vault kv/authentik/<app>-oidc
Authentik-->>App: id_token + access_token
App->>User: Session established
Apps using Authentik OIDC: ArgoCD, Grafana, Harbor, Nextcloud (via proxy provider), GitLab.
Blueprint pattern: OIDC provider configurations are stored as Authentik blueprints in ConfigMaps (core-components/authentik/resources/). They are applied automatically on Authentik startup — no manual UI configuration needed after a fresh install.
Defense-in-depth across 5 layers:
graph TB
subgraph L1[L1 - Network Perimeter]
MIKROTIK_FW[MikroTik firewall - Drop unsolicited inbound - No port-forwarding from WAN]
CF_WAF[Cloudflare Zero Trust - Only whitelisted domains exposed]
end
subgraph L2[L2 - Ingress WAF]
CROWDSEC[CrowdSec LAPI - IP reputation blocklist]
APPSEC2[CrowdSec AppSec - OWASP CRS rule set]
end
subgraph L3[L3 - Pod Network Policy]
CILIUM_CNP[Cilium CNP - Default-deny per namespace - Whitelist-only ingress egress]
end
subgraph L4[L4 - Runtime Security]
FALCO[Falco - eBPF syscall monitoring - privilege escalation detection]
SURICATA[Suricata - Deep packet inspection - intrusion signatures]
end
subgraph L5[L5 - Secret Security]
VAULT2[HashiCorp Vault - Encrypted at rest Raft - Audit log enabled]
COSIGN[Cosign image signing - Only signed images from Harbor]
end
L1 --> L2 --> L3 --> L4
L3 --> L5
style L1 fill:#fce4ec,stroke:#c62828
style L2 fill:#fff3e0,stroke:#e65100
style L3 fill:#e8f5e9,stroke:#2e7d32
style L4 fill:#e3f2fd,stroke:#1565c0
style L5 fill:#f3e5f5,stroke:#6a1b9a
Default-deny network policy: Every namespace with a CNP resource gets an implicit default-deny. Pods with no matching CNP have open networking (Cilium only enforces where policies exist). Best practice: always add a CNP when deploying a new namespace.
Falco uses eBPF to watch syscalls cluster-wide. Alerts go to VictoriaMetrics → vmalert → ntfy (homelab-security topic) for high-severity events.
CrowdSec community intelligence: the LAPI syncs with the CrowdSec CAPI every hour, pulling a community blocklist of known-bad IPs. Combined with local signal processing from the Istio gateway access logs (nginx-format).
graph TB
subgraph Infra[Infrastructure Layer waves -10 to 3]
CILIUM2[Cilium CNI - BPF networking and CNP]
METALLB2[MetalLB - LoadBalancer IPs]
CERTMGR[cert-manager - TLS certs]
ISTIO2[Istio Gateway - HTTPS ingress]
LONGHORN2[Longhorn - Block storage]
NFS[NFS StorageClasses - TrueNAS and Synology]
CROWDSEC2[CrowdSec - Ingress WAF]
end
subgraph SecretsMgmt[Secrets Layer waves 4-5]
VAULT3[Vault HA - 3-node Raft]
VSO2[VSO - Secret sync]
end
subgraph DNS_Layer[DNS Layer waves 6-7]
TECHNITIUM2[Technitium - 3-node HA]
EXTDNS[ExternalDNS - auto DNS records]
end
subgraph AuthLayer[Auth Layer wave 10]
AUTHENTIK2[Authentik - SSO and IdP]
end
subgraph ObsLayer[Observability Layer wave 11]
MON[VictoriaMetrics - and VictoriaLogs - and Grafana]
VELERO2[Velero - Backups to MinIO]
end
subgraph AppLayer[Application Layer waves 12-15]
HARBOR2[Harbor - Container registry]
NEXTCLOUD2[Nextcloud]
LIFEOPS2[LifeOps]
ENTE2[Ente Auth]
end
CILIUM2 --> METALLB2 --> ISTIO2
CERTMGR --> ISTIO2
CROWDSEC2 --> ISTIO2
LONGHORN2 --> SecretsMgmt
NFS --> ObsLayer
VAULT3 --> VSO2
VSO2 -->|injects secrets| AppLayer
VSO2 -->|injects secrets| ObsLayer
TECHNITIUM2 --> EXTDNS
EXTDNS -->|DNS records| TECHNITIUM2
AUTHENTIK2 -->|OIDC| AppLayer
AUTHENTIK2 -->|OIDC| ObsLayer
HARBOR2 --> LIFEOPS2
VELERO2 -->|backup| AppLayer
style VAULT3 fill:#e8f0fe,stroke:#4285f4
style AUTHENTIK2 fill:#fce8b2,stroke:#f9ab00
style ISTIO2 fill:#f96,stroke:#333
style CILIUM2 fill:#e8f4f8,stroke:#4a9aba