Building a Secure Homelab with Proxmox VE, pfSense, and Cilium
Published: May 25, 2026 | CommsNet
Your homelab shouldn't be a flat network where every container can talk to every service. That's the ISP's model — one big subnet, trust everything. But you're running your own infrastructure now. You get to do better.
In this article, I'll walk through building a layered, observable homelab using three technologies that complement each other beautifully: Proxmox VE for virtualization, pfSense for network segmentation, and Cilium for eBPF-based observability and micro-segmentation. By the end, you'll have a setup where a compromised container can't pivot to your storage array, your traffic flows are visible at the kernel level, and your firewall rules actually mean something because your VLANs enforce them.
Why This Stack?
Proxmox VE — The Foundation
Proxmox gives you KVM virtual machines and LXC containers on the same hypervisor, with a decent web UI, ZFS-backed storage, and cluster support. It's the closest thing to an enterprise data center you can run on repurposed desktop hardware.
What matters for security:
- Separate physical NICs for WAN, LAN, and management — don't trunk everything over one interface
- Linux Bridge isolation — each VLAN gets its own bridge, and bridges don't route between each other without explicit firewall rules
- AppArmor profiles for LXC containers — even your unprivileged containers get kernel-level confinement
pfSense — The Gatekeeper
pfSense sits at your network boundary and between your VLANs. It's not sexy, but it's reliable. FreeBSD's packet filter is battle-tested, and pfSense gives you a usable GUI on top of it.
What you get:
- Inter-VLAN firewalling — your IoT VLAN cannot reach your server VLAN unless you write a rule
- Alias-based rules — group IPs and ports by function, not by individual addresses
- VPN concentrator — WireGuard or OpenVPN for remote access without exposing services
- Traffic shaping — because your backup job shouldn't saturate your upload
Cilium — The Observer
This is where it gets interesting. Cilium uses eBPF to inject observability and security policies directly into the Linux kernel — no sidecar proxies, no iptables chaos. It sees every packet, every connection, every DNS lookup, and it can enforce policy at Layer 3/4 and Layer 7.
Why Cilium in a homelab:
- Hubble — real-time service map showing every connection between your workloads
- Network policies — Kubernetes-native micro-segmentation that pfSense can't see inside your cluster
- eBPF observability — kernel-level tracing without modifying applications
- Transparent encryption — WireGuard-based encryption between nodes, managed declaratively
Architecture Overview
┌─────────────┐
│ Internet │
└──────┬──────┘
│ WAN
┌──────┴──────┐
│ pfSense │
│ (VM on │
│ Proxmox) │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐
│ VLAN 10 │ │VLAN 20│ │ VLAN 30 │
│ Trusted │ │ IoT │ │ Servers │
│ LAN │ │ │ │ (K8s) │
└─────┬─────┘ └───────┘ └─────┬─────┘
│ │
┌─────┴─────┐ ┌───────┴────────┐
│ Workstations│ │ Proxmox Node │
│ Printers │ │ ┌─────────────┐│
│ Media │ │ │ K8s Cluster ││
└─────────────┘ │ │ + Cilium ││
│ │ + Hubble ││
│ └─────────────┘│
└─────────────────┘
Step 1: Proxmox Network Foundation
Physical NIC Assignment
Don't skip this. Using a single NIC with VLAN tagging works in a pinch, but separate physical interfaces eliminate a whole class of failure modes.
# Check your NICs
ip link show
# Assign roles in /etc/network/interfaces
# eno1 → WAN (passed through to pfSense VM)
# eno2 → vmbr0 (LAN - VLAN 10)
# eno3 → vmbr1 (Server - VLAN 30)
# eno4 → vmbr2 (IoT - VLAN 20)
Bridge Configuration
# /etc/network/interfaces on Proxmox host
# Management bridge (access your Proxmox web UI)
auto vmbr0
iface vmbr0 inet static
address 10.10.10.1/24
bridge-ports eno2
bridge-stp off
bridge-fd 0
# Server VLAN bridge
auto vmbr1
iface vmbr1 inet static
address 10.30.10.1/24
bridge-ports eno3
bridge-stp off
bridge-fd 0
# IoT VLAN bridge (isolated)
auto vmbr2
iface vmbr2 inet static
address 10.20.10.1/24
bridge-ports eno4
bridge-stp off
bridge-fd 0
Proxmox Firewall Basics
Enable the datacenter firewall, but keep it simple initially. Block all by default, allow only what's needed:
# /etc/pve/firewall/cluster.fw
[OPTIONS]
enable: 1
policy_in: DROP
policy_out: ACCEPT
[RULES]
# Allow SSH from management subnet only
IN ACCEPT -source 10.10.10.0/24 -p tcp -dport 22 -log nolog
# Allow Proxmox web UI from management subnet
IN ACCEPT -source 10.10.10.0/24 -p tcp -dport 8006 -log nolog
# Allow pfSense management
IN ACCEPT -source 10.10.10.0/24 -p tcp -dport 443 -dest 10.10.10.2 -log nolog
Step 2: pfSense as VLAN Firewall
VM Setup
Create pfSense as a VM with at least 2 vCPUs and 2GB RAM. Attach NICs:
| Interface | Bridge | Role |
|---|---|---|
| vtnet0 | WAN passthrough | Internet |
| vtnet1 | vmbr0 | LAN (VLAN 10) |
| vtnet2 | vmbr1 | Server (VLAN 30) |
| vtnet3 | vmbr2 | IoT (VLAN 20) |
Inter-VLAN Rules
This is where most homelabs fall apart. If your IoT devices can reach your NAS, you've already lost. Here's the principle:
Default deny. Allow only what's explicitly needed.
LAN → Any (Trusted)
# LAN interface — your workstations and admin machines
# Allow all outbound (your trusted users)
pass in on vtnet1 from 10.10.10.0/24 to any
Server → LAN (Selective)
# Server interface — K8s nodes, storage, services
# Allow established connections back
pass in on vtnet2 proto tcp from 10.30.10.0/24 to 10.10.10.0/24 port { 22, 443, 6443 } keep state
# Block everything else
block in on vtnet2 from 10.30.10.0/24 to 10.10.10.0/24
IoT → Only Internet (Locked Down)
# IoT interface — your smart devices, cameras, TVs
# Allow DNS and NTP outbound
pass in on vtnet3 proto { tcp, udp } from 10.20.10.0/24 to any port { 53, 123 }
# Allow HTTP/HTTPS outbound for firmware updates
pass in on vtnet3 proto tcp from 10.20.10.0/24 to any port { 80, 443 }
# Block ALL access to internal networks
block in on vtnet3 from 10.20.10.0/24 to { 10.10.10.0/24, 10.30.10.0/24, 10.10.10.1, 10.30.10.1 }
Aliases — Manage Rules at Scale
Don't hardcode IPs. Use aliases:
# pfSense aliases (Diagnostics → Aliases)
TRUSTED_NET = 10.10.10.0/24
SERVER_NET = 10.30.10.0/24
IOT_NET = 10.20.10.0/24
DNS_PORTS = 53
NTP_PORTS = 123
ADMIN_PORTS = 22, 443, 8006
Then your rules reference aliases, not IPs. When you add a new server subnet, update the alias, not every rule.
Step 3: Cilium for Kubernetes Observability and Micro-Segmentation
Installation
Deploy Cilium as your CNI on your K8s cluster running inside Proxmox VMs or LXC:
# Install Cilium via Helm
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium \
--namespace kube-system \
--set kubeProxyReplacement=strict \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true \
--set operator.replicas=1
Why kube-proxy Replacement Matters
Running kubeProxyReplacement=strict means Cilium replaces iptables-based kube-proxy entirely with eBPF. Benefits:
- Faster service routing — eBPF programs run in-kernel, no context switches to userspace
- Lower latency — direct packet processing at the socket layer
- Consistent observability — every connection goes through the same eBPF programs
- No iptables drift — one mechanism, not two
Hubble — See Every Connection
Hubble is Cilium's observability layer. It streams every network connection in real time:
# Port-forward Hubble Relay
kubectl port-forward -n kube-system svc/hubble-relay 4245:4245
# Watch all connections in real time
hubble observe --since 1m
# Filter by namespace
hubble observe --namespace production --since 5m
# Track DNS resolution failures (classic IoT misbehavior indicator)
hubble observe --type trace:to-endpoint:dns --verdict DROPPED
The Hubble UI gives you a visual service map. You'll see immediately if your Home Assistant container is phoning home to sketchy endpoints, or if your Nextcloud pod is trying to reach the Kubernetes API when it shouldn't.
Network Policies — Defense in Depth
pfSense controls traffic between VLANs. Cilium NetworkPolicies control traffic inside your cluster. Both layers matter.
Default Deny All Ingress
# default-deny.yaml — apply to every namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
Allow Specific Service Communication
# nextcloud-policy.yaml — Nextcloud can reach its DB and Redis, nothing else
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: nextcloud-ingress
namespace: home-services
spec:
podSelector:
matchLabels:
app: nextcloud
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: postgres-nextcloud
ports:
- port: 5432
- to:
- podSelector:
matchLabels:
app: redis-nextcloud
ports:
- port: 6379
- to: [] # Allow DNS
ports:
- port: 53
protocol: UDP
CiliumNetworkPolicy — L7 Visibility
Standard Kubernetes NetworkPolicies are L3/L4. Cilium extends this to L7:
# cilium-l7-policy.yaml — restrict Nextcloud egress at HTTP level
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: nextcloud-l7-egress
namespace: home-services
spec:
endpointSelector:
matchLabels:
app: nextcloud
egress:
- toFQDNs:
- matchName: "updates.nextcloud.com"
- matchName: "download.nextcloud.com"
toPorts:
- ports:
- port: "443"
rules:
http:
- method: GET
path: "/.*"
Now your Nextcloud instance can only make outbound GET requests to specific domains. If an attacker compromises it, they can't exfiltrate data to arbitrary endpoints — the eBPF program in the kernel drops the connection before it reaches the wire.
Step 4: Observability Pipeline
Metrics Flow
Cilium eBPF → Hubble → Prometheus → Grafana
↓
pfSense (netflow) → Prometheus → Grafana
↓
Proxmox (node metrics) → Prometheus → Grafana
Key Dashboards to Build
- Inter-VLAN traffic — who's talking to whom? Any IoT device hitting server subnets?
- DNS queries per namespace — spot DNS tunneling or C2 callbacks
- Connection drops per policy — are your policies actually working?
- pfSense rule hits — which rules fire most? Tune your alias groups.
- eBPF program latency — should be sub-microsecond. If it spikes, you've got a problem.
Alerting Rules
# Prometheus alerts for your homelab security
groups:
- name: homelab-security
rules:
- alert: IoTDeviceReachingServerNet
expr: |
hubble_flows_total{
source_namespace="iot",
destination_namespace="servers"
} > 0
for: 1m
labels:
severity: critical
annotations:
summary: "IoT device reaching server network"
- alert: UnexpectedDNSQueries
expr: |
count(hubble_dns_queries_total) by (namespace, qname)
> 100
for: 5m
labels:
severity: warning
annotations:
summary: "Unusual DNS query volume from {{ $labels.namespace }}"
- alert: CiliumPolicyDrop
expr: |
cilium_policy_verdict_total{verdict="dropped"} > 0
for: 2m
labels:
severity: info
annotations:
summary: "Cilium policy dropping traffic — expected behavior"
Security Best Practices Checklist
Proxmox
- [ ] Enable Proxmox firewall at datacenter level
- [ ] Use separate physical NICs for WAN, LAN, management
- [ ] Run LXC containers as unprivileged with AppArmor profiles
- [ ] Enable 2FA on Proxmox web UI
- [ ] Regular ZFS snapshots with automated send to offsite
- [ ] Keep Proxmox updated — subscribe to enterprise repo or use no-subscription
pfSense
- [ ] Default deny on all interfaces except LAN
- [ ] Use aliases, not hardcoded IPs, in rules
- [ ] Enable pfBlockerNG for ad/malware blocking at the gateway
- [ ] Set up WireGuard VPN for remote access — don't expose services
- [ ] Regular config backups (Automated via cron to offsite storage)
- [ ] Disable WAN responses — no ping, no admin interface on WAN
Cilium/Kubernetes
- [ ] Default deny all ingress NetworkPolicy in every namespace
- [ ] Use CiliumNetworkPolicy for L7 egress restrictions
- [ ] Enable Hubble for real-time observability
- [ ] Run
kubeProxyReplacement=strict— don't mix iptables and eBPF - [ ] Enable Cilium encryption (WireGuard) for inter-node traffic
- [ ] Audit NetworkPolicies regularly — use
kubectl get networkpolicies -Ato review
General
- [ ] All management interfaces behind VPN or local access only
- [ ] SSH key-only authentication — disable password auth
- [ ] Automated security updates (unattended-upgrades on Debian/Ubuntu hosts)
- [ ] Centralized logging to a write-once destination
- [ ] Document your network topology — future you will thank present you
What This Gets You
| Threat | Mitigation |
|---|---|
| Compromised IoT device | Can't reach server VLAN (pfSense drops it) |
| Lateral movement in K8s | NetworkPolicy default deny + Cilium L7 rules |
| Data exfiltration from container | Cilium FQDN egress policy + DNS monitoring |
| Unauthorized remote access | WireGuard VPN, no exposed ports |
| Blind spots | Hubble service map + Prometheus alerts |
| Insider threat (rogue admin) | Separate management VLAN + audit logging |
Cost Breakdown
| Component | Hardware | Cost |
|---|---|---|
| Proxmox host | Refurbished Dell OptiPlex (i5, 32GB RAM, 2TB NVMe) | ~$200 |
| NICs | Intel X710-T2L (2.5GbE, SR-IOV capable) | ~$80 each × 2 |
| pfSense | VM on Proxmox (free CE edition) | $0 |
| Cilium | Open source | $0 |
| Switch | Used managed switch (VLAN-capable) | ~$40 |
| Total | ~$400 |
That's enterprise-grade network segmentation for less than a single rack-mount firewall license.
Next Steps
- Start with Proxmox — get your VMs running, assign NICs properly
- Add pfSense — configure VLANs before you deploy services
- Deploy K8s with Cilium — enable Hubble from day one
- Layer in policies — default deny first, then add specific allows
- Observe — watch Hubble for a week before you think you know your traffic patterns
The homelab that's invisible to its own admin is the homelab that gets owned. Build with observability from day one, and you'll catch problems when they're misconfigurations, not breaches.
CommsNet builds secure, observable infrastructure. More at wiki.commsnet.org
Tags: #homelab #proxmox #pfsense #cilium #ebpf #kubernetes #networking #security #selfhosted









