DEV Community

Luna Commsnet
Luna Commsnet

Posted on • Edited on • Originally published at wiki.commsnet.org

Building a Secure Homelab with Proxmox VE, pfSense, and Cilium

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. Honestly, I was surprised how quickly it becomes second nature once you get past the initial learning curve.

What matters for security is straightforward

  • 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 with pfSense

  • 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, with no sidecar proxies and 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     ││
                               │  └─────────────┘│
                               └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Proxmox Network Foundation

Proxmox Network Configuration

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. I learned this the hard way when a single trunked NIC failed and took down every VLAN at once during a firmware update.

# 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)
Enter fullscreen mode Exit fullscreen mode

Now configure your bridges in /etc/network/interfaces.

# /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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2: pfSense as VLAN Firewall

pfSense VLAN Rules and Aliases

Create pfSense as a VM with at least 2 vCPUs and 2GB RAM. Attach NICs as follows

Interface Bridge Role
vtnet0 WAN passthrough Internet
vtnet1 vmbr0 LAN (VLAN 10)
vtnet2 vmbr1 Server (VLAN 30)
vtnet3 vmbr2 IoT (VLAN 20)

This is where most homelabs fall apart. If your IoT devices can reach your NAS, you've already lost. The principle is simple.

Default deny. Allow only what's explicitly needed.

LAN to 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
Enter fullscreen mode Exit fullscreen mode

Server to 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
Enter fullscreen mode Exit fullscreen mode

IoT to 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 }
Enter fullscreen mode Exit fullscreen mode

Don't hardcode IPs. Use aliases instead.

# 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
Enter fullscreen mode Exit fullscreen mode

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

Cilium Installation and kube-proxy Replacement

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
Enter fullscreen mode Exit fullscreen mode

Running kubeProxyReplacement=strict means Cilium replaces iptables-based kube-proxy entirely with eBPF. The benefits are significant

  • 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 for Seeing 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
Enter fullscreen mode Exit fullscreen mode

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. This surprised me the first time I spun it up, I caught a smart plug making DNS requests to a Chinese IP within ten minutes of enabling Hubble.

Network Policies for 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

CiliumNetworkPolicy for 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: "/.*"
Enter fullscreen mode Exit fullscreen mode

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, Dashboards, and Alerting

Cilium eBPF → Hubble → Prometheus → Grafana
     ↓
pfSense (netflow) → Prometheus → Grafana
     ↓
Proxmox (node metrics) → Prometheus → Grafana
Enter fullscreen mode Exit fullscreen mode

These are the key dashboards to build

  1. Inter-VLAN traffic. Who's talking to whom? Any IoT device hitting server subnets?
  2. DNS queries per namespace. Spot DNS tunneling or C2 callbacks.
  3. Connection drops per policy. Are your policies actually working?
  4. pfSense rule hits. Which rules fire most? Tune your alias groups.
  5. eBPF program latency. Should be sub-microsecond. If it spikes, you've got a problem.

Now set up your 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"
Enter fullscreen mode Exit fullscreen mode

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 -A to 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

  1. Start with Proxmox, get your VMs running, assign NICs properly
  2. Add pfSense, configure VLANs before you deploy services
  3. Deploy K8s with Cilium, enable Hubble from day one
  4. Layer in policies, default deny first, then add specific allows
  5. 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


This article is also published on:

Originally published at wiki.commsnet.org. Follow CommsNet for more homelab, networking, and self-hosting content.

Sponsor our work: GitHub Sponsors

Top comments (0)