finn lippok.dev

Homelab

github.com/FinnPL/Homelab-Setup →

Three sites: a Kubernetes cluster at home, a container host on a secondary site, and a public-facing server in Oracle Cloud.

Hardware
Cluster host
Intel NUC running Proxmox VE
Out-of-band
JetKVM on the NUC, including power control
Cluster
Talos Linux: one control-plane VM and four workers
Workers
Three Raspberry Pi 4, plus one VM on the NUC for database workloads
Storage
NFS from a dedicated container; node-local volumes on the database VM
Second site
Raspberry Pi 3 running Docker Compose
Cloud edge
Oracle Cloud ARM instance running NixOS
Front of the homelab rack
Front
Rear cabling of the homelab rack
Back
Network
Gateway and switching
Ubiquiti UniFi, with VLANs and zone-based firewall rules declared in Terraform
Cluster networking
Cilium, replacing kube-proxy; Gateway API for ingress
DNS
Blocky for filtering, Unbound for DNSSEC validation and DoT upstream
Tunnels
WireGuard between the edge and home; Tailscale for CI runners and remote access
Public DNS and certificates
Cloudflare, Let's Encrypt DNS-01
Tooling
Infrastructure
Terraform for networking and machines; Ansible and Nix for VM and LXC configuration
Delivery
GitHub Actions, ArgoCD, Renovate
Secrets
HashiCorp Vault. Action runners authenticate with a GitHub OIDC token to get a short-lived token scoped to that workflow, the cluster authenticates with a ServiceAccount token, and SSH host and client certificates are signed by Vault CAs.
Policy
Kyverno at admission in enforce mode, with the same policy set and exceptions run against every rendered chart in CI. Tetragon for eBPF runtime tracing. Trivy scans code in CI.
Observability
Prometheus and Alertmanager, Loki for logs, Alloy for collection, Grafana, Gatus
Design decisions

Storage

One goal when making architectural decisions was minimising SD card wear. That is why the default storage class is NFS, from an HDD-backed LXC on the NUC. The databases, which need block storage, run on a tainted worker that is also a VM on the Intel NUC, backed by an SSD.

These are temporary solutions. Once I get a new NAS, both move to Democratic CSI on TrueNAS.

DNS

Clients have a DoH profile pointing them at a domain that resolves to the public edge. HAProxy does not terminate the TLS; instead it relays the encrypted traffic across the WireGuard tunnel, and the handshake completes against the homelab.

Blocky terminates the session and filters using blocklists, then hands off to Unbound for DNSSEC validation. Unbound's upstream query leaves over DoT to 1.1.1.1, inside a ProtonVPN tunnel.

Cloudflare answers the query but sees a ProtonVPN exit address. ProtonVPN carries the traffic but the payload is DoT, so it sees only that a DNS connection happened. Neither side holds both halves, which is the same separation Oblivious DNS was built to provide, but self-hosted.

A canary name serves as the check. Public DNS points it at a dead IP, and a custom zone in Blocky points it somewhere real, so it only resolves correctly when the query actually went through this path.

DNS resolution path: end devices send DoH to the cloud edge, which passes TLS through a WireGuard tunnel to Blocky, then Unbound, which forwards over DoT through a VPN tunnel to Cloudflare.
Inventory

Generated from charts/ and the Terraform layers.

from 028c6d0 ·

applications
17 reconciled by ArgoCD
platform
5 charts installed by Terraform
charts
17 wrapper charts, 17 upstream versions pinned

Platform

  • argo-cd argocd argo-cd 10.11.1

    Installed by Terraform. From then on it deploys everything else from this repository.

  • argocd-config argocd

    ApplicationSet: Reconciles the cluster against this repository.

  • cert-manager cert-manager cert-manager v1.21.3

    Issues the wildcard certificate from Let's Encrypt over a DNS-01 challenge.

  • cilium kube-system cilium 1.20.4

    Replaces kube-proxy as CNI, advertises LoadBalancer addresses, Gateway API for HTTPRoutes.

  • cluster-namespaces

    Namespaces and their Pod Security Admission labels.

  • cnpg cnpg-system cloudnative-pg 0.29.1

    Postgres operator for shared DB.

  • crossplane crossplane-system crossplane 2.4.3

    Provisions Postgres databases and PgBouncer poolers and writes the credentials to a secret.

  • csi-driver-nfs kube-system csi-driver-nfs 4.13.6

    Backs the default storage class with the NFS export.

  • external-secrets external-secrets external-secrets 2.14.0

    Pulls secrets from Vault for Services. The cluster authenticates with a short-lived ServiceAccount token.

Networking

  • dns dns

    Blocky for filtering and caching, with an Unbound sidecar for DNSSEC validation, prefetching and DoT forwarding upstream.

  • gateway-external-routes gateway

    Gateway API routes for services hosted outside the cluster.

  • tailscale tailscale tailscale-operator 1.104.2

    Tailscale operator. GitHub Actions runners join the tailnet to reach the cluster.

Identity

  • authentik authentik authentik 2026.8.5

    Single sign-on for the services.

Policy

  • kyverno kyverno kyverno-policies 3.9.2

    The policy set for baseline pod hardening, enforced at admission.

  • kyverno-operator kyverno kyverno 3.9.2

    The Kyverno admission controller. CI runs the same policy set against rendered charts before merge.

  • tetragon tetragon tetragon 1.7.1

    eBPF runtime observability. The tracing policies are observe-only.

Observability

  • alloy monitoring alloy 1.13.1

    Telemetry collection. Scrapes nodes, Proxmox syslog and UniFi syslog.

  • gatus gatus gatus 1.5.0

    Black-box health checks and alerting.

  • kube-prometheus-stack monitoring kube-prometheus-stack 92.3.1

    Prometheus, Alertmanager and Grafana.

  • loki monitoring loki 7.3.0

    Log storage, queried from Grafana.

Storage

  • local-path-provisioner local-path-storage local-path-provisioner 0.0.37

    Node-local volumes on the database node. Everything else uses the NFS storage class.

Applications

  • it-tools it-tools

    A self-hosted collection of small conversion and encoding utilities.