Hermes
A separate agent VM manages context, instructions, memory, and tool orchestration. Model calls are one step in that workflow.
Architecture
One environment spanning virtualization, AI, security, and observability. Everything is designed, built, and operated as a single system. This is how the pieces actually fit together: the topology, the dataflows, and the decisions behind them.
Virtualization
A nine-node Proxmox cluster with HA, ZFS, SDN, and GPU passthrough.
๐งAI / Inference
Local-first inference across multiple GPUs, vLLM, SGLang, LiteLLM routing, and Hermes orchestration.
๐ก๏ธSecurity
Distributed CrowdSec, FortiGate edge, DNS failover, all infra over Tailscale.
๐Observability
Heimdall: metrics, logs, and SIEM into a single tailnet-only pane of glass.
Two WAN links into a FortiGate edge, a local Proxmox cluster, a cloud node, a public DMZ, and a second homelab, all stitched together by a Tailscale control plane so management never crosses the public internet.
Edge โ Core โ Sites
WAN Uplinks
Fiber 2.5 GbE
Primary
Cable 950/40
Backup
Edge
FortiGate SD-WAN
Failover ยท SLA steering ยท IPS / UTM ยท threat feeds
Resolution & Core
Technitium DNS
โ Unbound
Pi-hole
โ Unbound
Core Switch
VLAN trunk ยท LACP
Sites & Segments
Local PVE Cluster
chromium ยท palladium ยท osmium ยท prometheus ยท rhenium ยท 10.0.0.0/24
DMZ ยท thallium
nginx + Docker ยท public
Cloud PVE1
cktech-nginx ยท 10.10.10.0/24
Azure ยท ckel-web-01
public web ยท CrowdSec bouncer
Second Homelab
Lab40 VLAN โ Lab-40F 90G ยท Hyper-V ร2 + Veeam
Backup & DR
Synology NAS
Active Backup ยท M365 / Workspace
Proxmox Backup Server
dedup ยท encrypted
Wasabi S3
offsite ยท 3-2-1
Control Plane Overlay
๐ The Tailscale mesh spans every site. All infra comms (SSH, LAPI, Wazuh, dashboards) ride the tailnet with strict ACLs and per-session SSH auth.
The edge is a FortiGate bonding a 2.5 GbE fiber line with a cable circuit into one resilient pipe, where SD-WAN health checks fail traffic over if a link drops and SLA-based steering keeps latency-sensitive workloads on the best path. Behind it, a dual-resolver DNS setup and a VLAN-trunked core fan out to the local Proxmox cluster on 10.0.0.0/24, a cloud Proxmox node on its own 10.10.10.0/24 SDN, a public DMZ, and a separate second homelab on its own VLAN and firewall.
The piece that ties it all together is Tailscale. Rather than exposing management interfaces, every site joins one tailnet and all administrative traffic (SSH, the CrowdSec LAPI, the Wazuh indexer, Grafana and Prometheus) rides that overlay with strict ACLs and authenticated SSH on every session. The only things deliberately facing the public internet are a hardened web front door and a single threat-feed endpoint, both isolated from the internal estate.
One FortiGate 90G doing real enterprise work. SD-WAN, server and link load balancing, full deep-SSL inspection, and Layer-7 filtering all run without giving up line-rate throughput.
Speeds & Feeds ยท Line-Rate With the Brain On
SD-WAN
Load Balancing ยท 1 of 2
Load Balancing ยท 2 of 2
Deep inspection
Application layer
The edge is a single FortiGate 90G pushed well past a typical home deployment. SD-WAN bonds a 2.5 GbE fiber primary with a 1 GbE cable backup in the wan-failover-link zone, where Performance SLA health probes drive both automatic failover and application-aware steering. When configured for it, SD-WAN also spreads sessions across both links at once by bandwidth ratio for aggregate throughput.
On the application side, a Virtual Server offloads SSL and load-balances across a health-checked nginx pool, while Full SSL Inspection acts as a controlled man-in-the-middle: the FortiGate terminates two TLS sessions, re-signing to managed clients with its own CKEL_ROOT_CA while validating each origin's real certificate. That decrypted stream is what makes Layer-7 filtering possible. Application control, web filtering, IPS, antivirus, DNS filtering and DLP all inspect the payload in the clear before it is re-encrypted and a single policy verdict is applied. The notable part is doing all of this at 2.5 Gbps symmetric with inspection fully enabled.
A nine-node Proxmox cluster for server workloads with HA failover, ZFS everywhere, and segmented SDN, where KVM runs Windows, Linux, and macOS (via OpenCore) guests, so any OS can be spun up and tested on real hardware, with VFIO passthrough for GPU-bound work.
Proxmox VE Cluster
Corosync quorum ยท HA ยท live migration
ZFS Storage
NVMe mirrors ยท RAIDz2 ยท zstd ยท scrubs
SDN
VLAN-aware bridges ยท VXLAN/EVPN ยท LACP
Cloud-Init / IaC
golden templates ยท Terraform ยท Ansible
Pre-copy memory migration
The compute layer is a nine-node Proxmox VE cluster spanning everything from an EPYC server to small-form-factor boxes, tied together with Corosync quorum and HA so guests live-migrate and restart automatically when a node drops. Storage is ZFS end to end. NVMe mirrors and RAIDz2 pools run compression and scheduled scrubs, and their snapshots feed both replication between nodes and the backup chain.
Windows guests
VirtIO drivers ยท GPU passthrough when needed
Linux guests
any distro ยท cloud-init golden templates
macOS guests
OpenCore + KVM ยท cross-platform testing
Because everything runs on KVM, the cluster can boot virtually any operating system, from Windows and any Linux distro to macOS via OpenCore. A guest is available for testing on essentially any OS environment on real hardware, without dedicated machines or dual-booting.
Workflow
Enable IOMMU on the host
amd_iommu=on ยท iommu=pt ยท isolate the IOMMU group
Bind GPU to vfio-pci
blacklist host driver ยท stub the device at boot
q35 + OVMF guest
PCIe passthrough ยท GPU + audio function ยท reset handling
Guest = near bare-metal GPU
RTX 4090 inference VM ยท isolated compute node
For workloads that need real hardware, VFIO passthrough isolates a GPU's IOMMU group, binds it to vfio-pci, and hands it straight into a q35/OVMF guest, so a full RTX 4090 runs inside a VM at close to bare-metal speed as a dedicated inference node.
Separate from the cluster: a single Arch Linux daily driver (Ryzen 9950X3D ยท RTX 5090) where KVM + VFIO passthrough hands a real GPU to a Windows guest, and Looking Glass mirrors it back as a lossless, near bare-metal desktop.
Arch Linux (KVM + VFIO) โ Windows guest ยท lossless video
The daily driver runs Arch Linux as the KVM host. VFIO passthrough isolates the GPU's IOMMU group, binds it to vfio-pci, and hands it into a q35/OVMF Windows guest, so the VM drives a real GPU at close to bare-metal speed.
Looking Glass then copies each guest frame through an IVSHMEM shared-memory region into a client on the Arch host. The result is an uncompressed, low-latency desktop with no video encode and no network hop, so 3D apps and CUDA workloads inside Windows run at effectively bare-metal performance, in a window right alongside Linux.
Why it works ยท clean IOMMU groups
The split only works because the GPUs land in clean IOMMU groups. The discrete RTX 5090, along with its HDMI-audio function, is stubbed to vfio-pci at boot and handed whole to the guest, while the CPU's Radeon iGPU keeps the native amdgpu driver and drives the Arch desktop, so the host still has a display to render the Looking Glass client into.
This is the same approach on both the cluster and the workstation: I spec motherboards with proper IOMMU support up front so devices land in clean groups on their own. ACS-override patches work and have been reliable enough when I've needed them, but choosing hardware that groups correctly removes the need for them entirely. The payoff is fewer kernel patches, cleaner upgrades, and predictable passthrough.
Every dataset rides a 3-2-1 footing, and the restores are tested, not assumed. Each platform has its own chain, all converging on encrypted offsite copies in Wasabi.
What 3-2-1 means
The unofficial +1: restores are rehearsed on a schedule. A backup you haven't restored is a hope, not a recovery plan.
The rule, visualized
Daily driver
Cluster + cloud node
Hyper-V lab
Client-side + at rest
The Arch daily driver runs btrfs with a flat subvolume layout, and snapper keeps an automated ~30-day window of local snapshots. Pre/post-pacman and timeline snapshots make a bad upgrade a one-command rollback. A systemd-timed restic job then takes encrypted, file-level copies of /home and /data to a Synology NAS, which HyperBackup mirrors offsite to Wasabi.
The Proxmox cluster leans on Proxmox Backup Server for incremental, deduplicated, client-encrypted guest backups, with ZFS replication between nodes for warm standby; PBS and the offsite cloud PVE node both sync to Wasabi. The Hyper-V lab uses Veeam for application-consistent full image backups daily plus incrementals, kept hot locally for ~45 days before older restore points age out to a Wasabi capacity tier.
Privacy is non-negotiable: restic and Veeam encrypt client-side, so keys never leave the host and only ciphertext crosses the wire. Wasabi adds its own encryption at rest and stores everything deduplicated. The provider never holds a key or sees plaintext.
Redundancy is the first line of defense. It keeps data online when a disk dies, before any backup is needed. Every tier is built to lose drives without losing data, all of it checksummed and scrubbed against silent corruption.
Built to lose disks
The primary storage host runs a RAIDZ2 pool with ZFS double parity, so it can lose two drives at once and stay online, which matters most on the wide, high-capacity vdevs where a second disk can fail during a rebuild. The rest of the cluster runs ZFS mirrors (RAID1): each vdev keeps a full copy, so a failed drive resilvers fast from its twin with minimal performance hit, the right trade for the smaller NVMe pools backing VMs.
The Synology NAS uses SHR (Synology Hybrid RAID) with btrfs under the hood, which adds flexible redundancy across mixed-size drives (no wasted capacity from matching the smallest disk) plus btrfs block checksums and self-healing. Across all three, ZFS and btrfs checksum every block and scheduled scrubs catch and repair bit rot before it spreads. Redundancy isn't a substitute for the 3-2-1 backups above. It's what keeps everything serving while a disk gets replaced.
CUDA workloads run on native hosts, GPU passthrough VMs, and containers. Model requirements and available capacity guide placement across the daily-use GPUs and Spark cluster.
Same toolchain, any context
Workload placement
CUDA reaches workloads through three deployment paths: native execution on the Arch desktop, a VFIO passthrough guest with its own GPU driver, or containers with the NVIDIA Container Toolkit. PyTorch, vLLM, SGLang, and ComfyUI use the GPU through the software environment configured for each workload. Moving a workload still requires compatible drivers, dependencies, and GPU support.
The RTX 5090, RTX 4090, and two-node DGX Spark cluster are my primary daily compute systems. The smaller cards support lighter inference, testing, and CI. I allocate work around model memory requirements and available capacity; concurrent jobs still compete for the same physical GPU resources. AMD and integrated GPUs handle work outside the CUDA stack.
Container GPU sharing
The toolkit exposes GPU devices to containers on the Arch host or inside a GPU passthrough VM. Workload concurrency depends on the cardโs available memory and compute capacity.
One GPU, many containers
Driver placement
A Proxmox passthrough VM receives a physical GPU through vfio-pci and runs the NVIDIA driver inside the guest. Docker and the NVIDIA Container Toolkit then expose that GPU to selected containers. The same container pattern works directly on the Arch host for native GPU workloads.
The kernel driver stays on the container host, whether that host is a VM or a physical machine. Images carry application dependencies and their required CUDA runtime, while the toolkit provides device access and driver integration. Containers separate software environments; GPU memory and compute still need capacity planning.
AI / Request path
Hermes, Open WebUI, OpenCode, Jarvis, and applications reach model services through LiteLLM. The gateway routes requests; the inference endpoints run the models.
A separate agent VM manages context, instructions, memory, and tool orchestration. Model calls are one step in that workflow.
The model gateway provides named routes and scoped API access. Inference stays on the selected vLLM backend, with SGLang also used in the serving stack.
Browser chat runs in its own LXC container and sends model requests through the gateway. Tailscale provides private connectivity to services.
Distributed inference / DGX Spark
The primary node hosts the vLLM API and coordinates distributed inference with the worker. LiteLLM sends client requests to the primary; the pair exchanges inference data over a separate high-speed link.
Each Spark has 128 GB of unified memory, giving the pair 256 GB in aggregate. Distributed serving assigns model work to both nodes; the machines retain separate memory. Runtime overhead, model format, and context requirements determine usable capacity.
The ConnectX-7 connection is rated at 200 Gb/s, or 25 GB/s before overhead. RoCE carries RDMA traffic over Ethernet, and NCCL handles collective communication for distributed GPU work. That dedicated path carries node-to-node exchanges while the regular network carries client requests and management traffic. The rated link speed is separate from measured inference throughput.
Agent execution
Hermes assembles context, calls the model through LiteLLM, and handles the next action. Tool permissions and approvals govern execution; results return to the agent for the next step.
ComfyUI provides the separate image and video workflow layer, while n8n connects scheduled jobs and service integrations. These workloads use the same broader infrastructure without making every task an LLM request.
Explore the daily AI workflow โA distributed CrowdSec engine that turns a detection anywhere into enforcement everywhere, a FortiGate perimeter that consumes it, resilient DNS, and a management plane that lives entirely on Tailscale.
Distributed engine
Detection ยท log processors
PVE / Linux hosts
parse logs ยท match scenarios
Edge nginx
bouncer + parser
Azure website
nginx bouncer ยท sensor + enforce
Central ยท Local API (single source of truth)
CrowdSec LAPI
10.0.0.23:8080 ยท Postgres ยท profiles โ decisions ยท + CAPI & console lists
Enforcement ยท bouncers
FortiGate
external threat feed
nginx bouncers
edge + Azure
PVE firewall
nftables ipset (planned)
Detection โ enforcement loop
Inputs
Edge agents / bouncers
over Tailscale
Azure site attacks
reported as sensor
CAPI + console lists
community blocklists
Engine
CrowdSec LAPI
union of all decisions ยท ~75k entries
Mirror ยท thallium (public DMZ)
blocklist-mirror :41412
bouncer ยท refresh 10s โ nginx (wildcard cert)
Published
threat.cktechnology.io/crowdsec.txt
public ยท plaintext IP/CIDR list
Consumed
FortiGate external feed (pull)
address object โ IPv4 DENY policy
Resilient resolution
Clients
LAN clients
DNS forced by FortiGate
Resolvers (health-check failover)
Technitium
primary
Pi-hole
secondary
Recursion
Unbound
recursive ยท DNSSEC validating
Isolated by design (public)
Ephemeral Kali Linux
disposable pentest containers ยท external ranges (e.g. TryHackMe) over OpenVPN
Browser isolation
browser-in-a-browser ยท right-click โ open in Kasm ยท safe link detonation
Remote support ยท split exposure
Self-hosted services
ScreenConnect
ConnectWise Control ยท PVE1 VM ยท RMM / remote support
Hudu
self-hosted docs & runbooks ยท Tailscale-only
Split exposure
Relay (screlay) ยท public
session traffic only ยท agents/clients connect in
Web / Admin ยท Tailscale only
management console never faces the internet
Guardrails + resilience
PVE firewall + nftables
DC / node / guest rules ยท admin hidden behind the tailnet
Cloud fallback
cktech.screenconnect.com ยท if the on-prem instance is down
Access Model ยท Everything Over Tailscale
๐ All infra control-plane comms (CrowdSec LAPI, Wazuh manager & indexer, Grafana, Prometheus, and SSH) ride the Tailscale mesh with strict ACLs and per-session SSH auth. Hosts firewall with nftables, SSH is key-only, and the only deliberate public surfaces are the threat-feed endpoint, the isolated Kasm VM, and the ScreenConnect session relay. Every admin console stays on the tailnet.
Security runs as a distributed CrowdSec deployment: log processors across the hosts and edge parse logs and ship alerts to a central LAPI, a Postgres-backed, LAPI-only Security Engine that is the single source of truth, while bouncers query it for decisions. Because the processors don't talk to each other, the central API is what makes a detection on one box actionable everywhere, and all of that traffic stays on Tailscale.
The engine's full decision union (local bans plus the CAPI community feed and console lists, around 75k entries) is mirrored on the public thallium box and served as a plaintext list at threat.cktechnology.io/crowdsec.txt, which the FortiGate pulls in as an external threat feed and enforces with a DENY policy. An attack on the public Azure website is reported by its bouncer, lands in the feed, and is blocked at the perimeter for every other service, closing the detection-to-enforcement loop. DNS is just as deliberate: Technitium and Pi-hole both front a validating Unbound resolver, with FortiGate SD-WAN failing over between them so name resolution survives a resolver going down.
Two independent pipelines for logs and metrics, plus security overlays, unified in Grafana and reachable only over the tailnet.
Pipeline 1
Senders
FortiGate
udp/tcp :5514 ยท key=value
RFC3164
udp/tcp :514
RFC5424
tcp :601
Ingest ยท syslog-ng (native)
syslog-ng
tag source_type / app / host ยท JSON-escape ยท disk-buffer
Store
Loki :3100
HTTP push ยท low-cardinality labels ยท 30d
/var/log/remote
raw file archive (redundant)
View
Grafana
Loki Explorer ยท FortiGate dashboards (LogQL logfmt)
Pipeline 2
Exporters (file-based SD)
node_exporter
host CPU/mem/disk/net
cAdvisor
per-container
Proxmox + edge
targets/*.yml ยท hot reload
Scrape & store
Prometheus :9090
15s scrape ยท 30d / 40GB
Outputs
Alertmanager :9093
infra.yml rules โ routing
Grafana
node + cluster dashboards
Overlays
Sources (read-only, over Tailscale)
CrowdSec LAPI :6060
cs_* metrics โ Prometheus
Wazuh indexer :9200
OpenSearch ยท wazuh-alerts-4.x
Unified pane
Grafana
CrowdSec Overview ยท Wazuh SIEM (MITRE)
Heimdall runs two independent pipelines. Logs flow through a native syslog-ng that tags each stream with low-cardinality labels and pushes to Loki over HTTP, while also writing a raw file archive as a redundant copy; FortiGate gets a dedicated port and its key=value logs are parsed at query time with LogQL. Metrics are pull-based: Prometheus scrapes node_exporter, cAdvisor, and the Proxmox and edge hosts via file-based service discovery, so adding a target is a file edit and a hot reload, with alerts routed through Alertmanager.
Security telemetry rides alongside as read-only overlays, with CrowdSec LAPI metrics feeding Prometheus and the Wazuh indexer mounted as an OpenSearch datasource, so a single Grafana instance covers host health, cluster capacity, firewall traffic, and MITRE-mapped detections. Like the rest of the management plane, every piece of it is reachable only over Tailscale.
The same Heimdall pipelines as true flow graphs, with branches, merges, and labeled or observe-only edges, and animation tracing the direction each stream actually travels.
Flow 1
Flow 2
Flow 3
Flow 4
From Proxmox clusters and FortiGate edges to private AI, distributed security, and full observability, I design and operate resilient systems end to end.