<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luna Commsnet</title>
    <description>The latest articles on DEV Community by Luna Commsnet (@lunacommsnet).</description>
    <link>https://dev.to/lunacommsnet</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3988117%2F5be4f16c-b5ad-4f65-8b44-06c09ddea0f0.png</url>
      <title>DEV Community: Luna Commsnet</title>
      <link>https://dev.to/lunacommsnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lunacommsnet"/>
    <language>en</language>
    <item>
      <title>Self-Hosted Monitoring Stack: Zabbix + Grafana for Home Infrastructure</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:51:36 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/self-hosted-monitoring-stack-zabbix-grafana-for-home-infrastructure-53aj</link>
      <guid>https://dev.to/lunacommsnet/self-hosted-monitoring-stack-zabbix-grafana-for-home-infrastructure-53aj</guid>
      <description>&lt;h1&gt;
  
  
  Self-Hosted Monitoring Stack: Zabbix + Grafana for Home Infrastructure
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Published: June 15, 2026 | CommsNet&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;You know that feeling when something breaks and you only find out because the website is down? That's not monitoring — that's embarrassment detection. Real monitoring tells you &lt;em&gt;before&lt;/em&gt; things break. It shows you the memory leak that started three hours ago, the disk that's filling at 2% per day, the SSL certificate expiring in 12 days.&lt;/p&gt;

&lt;p&gt;Enterprise monitoring platforms (Datadog, New Relic, Splunk) cost hundreds to thousands per month. For a homelab, that's absurd. But running blind is worse. The answer: self-hosted &lt;strong&gt;Zabbix&lt;/strong&gt; for data collection and alerting, paired with &lt;strong&gt;Grafana&lt;/strong&gt; for visualization. Together, they give you enterprise-grade observability at the cost of the electricity to run them.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through deploying a complete Zabbix + Grafana monitoring stack on Proxmox, configuring agents across VLANs, building dashboards that actually tell you something, and setting up alerts that wake you up when they matter — not at 3 AM for a transient spike.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Zabbix + Grafana?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Monitoring Landscape
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Data Ownership&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Alerting&lt;/th&gt;
&lt;th&gt;Dashboards&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Datadog&lt;/td&gt;
&lt;td&gt;$15-23/host/mo&lt;/td&gt;
&lt;td&gt;Cloud (theirs)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus + Grafana&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zabbix + Grafana&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Medium-High&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent (with Grafana)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netdata&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Good (built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime Kuma&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Not Just Prometheus?
&lt;/h3&gt;

&lt;p&gt;Prometheus is the darling of the cloud-native world, and for good reason. But for homelab monitoring, Zabbix has advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent-based collection works across VLANs&lt;/strong&gt; — Prometheus pull-based scraping struggles with firewall rules between VLANs. Zabbix agents push data to the server (or use active checks), making firewall rules simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-discovery&lt;/strong&gt; — Zabbix can discover hosts, interfaces, and services automatically. With Prometheus, you're writing &lt;code&gt;prometheus.yml&lt;/code&gt; targets by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in templates&lt;/strong&gt; — Zabbix has 400+ out-of-the-box templates for everything from Linux to pfSense to Proxmox to SNMP devices. Prometheus requires exporters for everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger logic&lt;/strong&gt; — Zabbix triggers support expressions like "average of last 5 minutes &amp;gt; threshold AND last value &amp;gt; threshold". Prometheus alerting rules are powerful but harder to compose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana integration&lt;/strong&gt; — Zabbix data in Grafana gives you the best of both: Zabbix collection + Grafana visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where Grafana Fits
&lt;/h3&gt;

&lt;p&gt;Zabbix has its own dashboards, but they look like 2005. Grafana is the visualization layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beautiful, customizable dashboards&lt;/li&gt;
&lt;li&gt;Unified view across multiple data sources&lt;/li&gt;
&lt;li&gt;Annotation layers (deploy events, maintenance windows)&lt;/li&gt;
&lt;li&gt;Alerting with deduplication and routing&lt;/li&gt;
&lt;li&gt;Mobile-responsive (check your homelab from your phone)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────┐
│                    Monitoring Architecture                       │
│                                                                  │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────────┐  │
│  │ Proxmox  │   │ pfSense  │   │ Docker   │   │ IoT Devices  │  │
│  │ Agent    │   │ Agent    │   │ Agent    │   │ SNMP         │  │
│  │ (VLAN20) │   │ (VLAN10) │   │ (VLAN20) │   │ (VLAN30)    │  │
│  └────┬─────┘   └────┬─────┘   └────┬─────┘   └──────┬───────┘  │
│       │              │              │                 │          │
│       └──────────────┴──────┬───────┴─────────────────┘          │
│                             │                                     │
│                    ┌────────▼────────┐                            │
│                    │  Zabbix Server  │                            │
│                    │  (VLAN 20)      │                            │
│                    │  - Collection  │                            │
│                    │  - Alerting    │                            │
│                    │  - Triggers    │                            │
│                    └────────┬──────┘                            │
│                             │                                     │
│                    ┌────────▼────────┐                            │
│                    │    Grafana      │                            │
│                    │  (VLAN 20)      │                            │
│                    │  - Dashboards   │                            │
│                    │  - Visualization│                            │
│                    │  - Alert UI     │                            │
│                    └─────────────────┘                            │
│                                                                  │
│  Alert Channels: Telegram, Email, Webhook                       │
└────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Network Considerations (VLAN-Aware)
&lt;/h3&gt;

&lt;p&gt;Following our zero-trust VLAN architecture from the previous article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zabbix Server&lt;/strong&gt; lives on VLAN 20 (Servers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zabbix Agents&lt;/strong&gt; on VLAN 10 (Management) push data to server via active checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNMP polling&lt;/strong&gt; from Zabbix to VLAN 30 (IoT) requires explicit firewall allow rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; on VLAN 20, with an optional reverse proxy on VLAN 50 (Services) if you want external access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Firewall rules needed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow Zabbix agents → Zabbix server (active checks)
&lt;/span&gt;&lt;span class="n"&gt;ALLOW&lt;/span&gt;  &lt;span class="n"&gt;VLAN10&lt;/span&gt; → &lt;span class="n"&gt;VLAN20&lt;/span&gt;  &lt;span class="n"&gt;TCP&lt;/span&gt; &lt;span class="m"&gt;10051&lt;/span&gt;  — &lt;span class="s2"&gt;"Management agents → Zabbix"&lt;/span&gt;
&lt;span class="n"&gt;ALLOW&lt;/span&gt;  &lt;span class="n"&gt;VLAN20&lt;/span&gt; → &lt;span class="n"&gt;VLAN20&lt;/span&gt;  &lt;span class="n"&gt;TCP&lt;/span&gt; &lt;span class="m"&gt;10051&lt;/span&gt;  — &lt;span class="s2"&gt;"Server agents → Zabbix"&lt;/span&gt;

&lt;span class="c"&gt;# Allow Zabbix server → IoT (SNMP polling, if desired)
&lt;/span&gt;&lt;span class="n"&gt;ALLOW&lt;/span&gt;  &lt;span class="n"&gt;VLAN20&lt;/span&gt; → &lt;span class="n"&gt;VLAN30&lt;/span&gt;  &lt;span class="n"&gt;UDP&lt;/span&gt; &lt;span class="m"&gt;161&lt;/span&gt;    — &lt;span class="s2"&gt;"Zabbix SNMP poll IoT"&lt;/span&gt;

&lt;span class="c"&gt;# Allow Grafana access from Management VLAN
&lt;/span&gt;&lt;span class="n"&gt;ALLOW&lt;/span&gt;  &lt;span class="n"&gt;VLAN10&lt;/span&gt; → &lt;span class="n"&gt;VLAN20&lt;/span&gt;  &lt;span class="n"&gt;TCP&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;   — &lt;span class="s2"&gt;"MGMT → Grafana dashboard"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deployment on Proxmox
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Zabbix Server LXC
&lt;/h3&gt;

&lt;p&gt;Proxmox LXC containers are perfect for monitoring — low overhead, fast startup, full Linux userspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download Debian 12 LXC template&lt;/span&gt;
pveam download &lt;span class="nb"&gt;local &lt;/span&gt;debian-12-standard_12.2-1_amd64.tar.zst

&lt;span class="c"&gt;# Create container&lt;/span&gt;
pct create 200 &lt;span class="nb"&gt;local&lt;/span&gt;:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt; zabbix &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 4096 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--swap&lt;/span&gt; 2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cores&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--storage&lt;/span&gt; local-lvm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rootfs&lt;/span&gt; local-lvm:32 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--net0&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eth0,bridge&lt;span class="o"&gt;=&lt;/span&gt;vmbr0.20,ip&lt;span class="o"&gt;=&lt;/span&gt;10.0.20.10/24,gw&lt;span class="o"&gt;=&lt;/span&gt;10.0.20.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unprivileged&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--onboot&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why LXC instead of VM:&lt;/strong&gt; Zabbix doesn't need its own kernel. LXC gives you 95% of a VM's isolation with 5% of the overhead. Your monitoring shouldn't be the heaviest thing on the host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Install Zabbix Server + PostgreSQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enter the container&lt;/span&gt;
pct enter 200

&lt;span class="c"&gt;# Install PostgreSQL&lt;/span&gt;
apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postgresql postgresql-contrib

&lt;span class="c"&gt;# Create Zabbix database&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres createuser &lt;span class="nt"&gt;--pwprompt&lt;/span&gt; zabbix
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres createdb &lt;span class="nt"&gt;-O&lt;/span&gt; zabbix &lt;span class="nt"&gt;-E&lt;/span&gt; Unicode &lt;span class="nt"&gt;-T&lt;/span&gt; template0 zabbix

&lt;span class="c"&gt;# Add Zabbix repository&lt;/span&gt;
wget https://repo.zabbix.com/zabbix/7.2/debian/pool/main/z/zabbix-release/zabbix-release_7.2-1+debian12_all.deb
dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; zabbix-release_7.2-1+debian12_all.deb
apt update

&lt;span class="c"&gt;# Install Zabbix server, frontend, and agent&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; zabbix-server-pgsql zabbix-frontend-php zabbix-apache-conf zabbix-sql-scripts zabbix-agent2

&lt;span class="c"&gt;# Import initial schema&lt;/span&gt;
zcat /usr/share/zabbix-sql-scripts/postgresql/server.sql.gz | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; zabbix psql zabbix

&lt;span class="c"&gt;# Configure Zabbix server&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/zabbix/zabbix_server.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
DBHost=localhost
DBName=zabbix
DBUser=zabbix
DBPassword=YOUR_POSTGRES_PASSWORD_HERE
LogFile=/var/log/zabbix/zabbix_server.log
LogFileSize=50
DebugLevel=3
StartPollers=5
StartPollersUnreachable=2
StartTrappers=5
StartDiscoverers=2
StartHTTPPollers=2
CacheSize=64M
HistoryCacheSize=32M
TrendCacheSize=8M
ValueCacheSize=32M
Timeout=10
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Start services&lt;/span&gt;
systemctl restart zabbix-server zabbix-agent2 apache2
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;zabbix-server zabbix-agent2 apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Install Grafana
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add Grafana repository&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; apt-transport-https software-properties-common
wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /usr/share/keyrings/grafana.key https://apt.grafana.com/gpg.key

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/grafana.key] https://apt.grafana.com stable main"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/grafana.list

apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; grafana

&lt;span class="c"&gt;# Configure Grafana&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/grafana/grafana.ini &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[server]
http_addr = 10.0.20.11
http_port = 3000
domain = grafana.commsnet.local

[security]
admin_user = admin
admin_password = CHANGE_ME_IMMEDIATELY

[database]
type = sqlite3

[analytics]
reporting_enabled = false
check_for_updates = false

[auth.anonymous]
enabled = false
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl restart grafana-server
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;grafana-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Connect Grafana to Zabbix
&lt;/h3&gt;

&lt;p&gt;Install the Zabbix data source plugin in Grafana:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grafana-cli plugins &lt;span class="nb"&gt;install &lt;/span&gt;alexanderzobnin-zabbix-app
systemctl restart grafana-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Grafana UI (&lt;strong&gt;Configuration → Plugins → Zabbix&lt;/strong&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable the Zabbix app plugin&lt;/li&gt;
&lt;li&gt;Add data source:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; Zabbix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; Zabbix API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;http://10.0.20.10/zabbix/api_jsonrpc.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; Admin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; Your Zabbix admin password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trends:&lt;/strong&gt; Enable (use trends for long-term graphs)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Configuring Zabbix Agents
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Agent on Proxmox Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the Proxmox host itself&lt;/span&gt;
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; zabbix-agent2

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/zabbix/zabbix_agent2.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
Server=10.0.20.10
ServerActive=10.0.20.10
Hostname=proxmox-host
LogFile=/var/log/zabbix/zabbix_agent2.log
DebugLevel=3

# Custom metrics for Proxmox
UserParameter=pve.cluster.status,/usr/bin/pvesh get /cluster/status --output json 2&amp;gt;/dev/null | grep -c '"online"'
UserParameter=pve.vm.count,/usr/bin/qm list 2&amp;gt;/dev/null | wc -l
UserParameter=pve.ct.count,/usr/bin/pct list 2&amp;gt;/dev/null | wc -l
UserParameter=pve.storage.used[*],/usr/bin/pvesm status --storage &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt; --output json 2&amp;gt;/dev/null | grep -o '"used":[0-9]*' | cut -d: -f2
UserParameter=pve.storage.total[*],/usr/bin/pvesm status --storage &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt; --output json 2&amp;gt;/dev/null | grep -o '"total":[0-9]*' | cut -d: -f2
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl restart zabbix-agent2
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;zabbix-agent2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Agent on pfSense
&lt;/h3&gt;

&lt;p&gt;pfSense has a Zabbix agent package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System → Package Manager → Available Packages → pfSense-zabbix-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zabbix Server IP: 10.0.20.10
Zabbix Server Port: 10051
Hostname: pfsense
Enable active checks: Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pfSense-specific items to monitor:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CARP status (if using HA)&lt;/li&gt;
&lt;li&gt;Gateway quality (packet loss, latency, jitter)&lt;/li&gt;
&lt;li&gt;State table utilization&lt;/li&gt;
&lt;li&gt;DHCP lease counts per VLAN&lt;/li&gt;
&lt;li&gt;Firewall rule denials per VLAN (from our zero-trust setup)&lt;/li&gt;
&lt;li&gt;OpenVPN/WireGuard client counts&lt;/li&gt;
&lt;li&gt;Interface traffic per VLAN&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Agent on Docker Hosts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml for Zabbix agent&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;zabbix-agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zabbix/zabbix-agent2:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zabbix-agent&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ZBX_SERVER_HOST=10.0.20.10&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ZBX_HOSTNAME=docker-host-01&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ZBX_ACTIVE_ALLOW=true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/:/hostfs:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker-specific metrics:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Additional UserParameters for Docker&lt;/span&gt;
&lt;span class="s"&gt;UserParameter=docker.container.count,/usr/bin/docker ps -q | wc -l&lt;/span&gt;
&lt;span class="s"&gt;UserParameter=docker.container.running,/usr/bin/docker ps --filter status=running -q | wc -l&lt;/span&gt;
&lt;span class="s"&gt;UserParameter=docker.image.count,/usr/bin/docker images -q | wc -l&lt;/span&gt;
&lt;span class="s"&gt;UserParameter=docker.volume.count,/usr/bin/docker volume ls -q | wc -l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Zabbix Host Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adding Hosts in Zabbix UI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Configuration → Hosts → Create Host:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Templates&lt;/th&gt;
&lt;th&gt;Groups&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;proxmox-host&lt;/td&gt;
&lt;td&gt;Linux by Zabbix agent, Proxmox VE by Zabbix&lt;/td&gt;
&lt;td&gt;Servers&lt;/td&gt;
&lt;td&gt;Agent: 10.0.20.5:10051&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pfsense&lt;/td&gt;
&lt;td&gt;pfSense by Zabbix&lt;/td&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Agent: 10.0.10.1:10051&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker-host-01&lt;/td&gt;
&lt;td&gt;Linux by Zabbix agent, Docker by Zabbix&lt;/td&gt;
&lt;td&gt;Servers&lt;/td&gt;
&lt;td&gt;Agent: 10.0.20.20:10051&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unifi-switch&lt;/td&gt;
&lt;td&gt;SNMP Generic, Ubiquiti Switch&lt;/td&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;SNMP: 10.0.10.2:161&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cisco-2960&lt;/td&gt;
&lt;td&gt;SNMP Generic, Cisco Switch&lt;/td&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;SNMP: 10.0.10.3:161&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Template Customization
&lt;/h3&gt;

&lt;p&gt;Zabbix templates are good out of the box but need tuning for homelab scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux by Zabbix agent — Adjust trigger thresholds:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Homelab Adjusted&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU load &amp;gt; 5min per core&lt;/td&gt;
&lt;td&gt;5 per core&lt;/td&gt;
&lt;td&gt;80% sustained 10min&lt;/td&gt;
&lt;td&gt;Homelab CPUs burst, don't alert on spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Available memory &amp;lt; 20%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Homelab hosts use more memory; 20% is too sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk space &amp;lt; 20%&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Small disks fill faster; 20% on 100GB = 20GB free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap usage &amp;gt; 50%&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;Some swap usage is normal in homelabs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;pfSense template — Add custom items:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Custom pfSense items via UserParameter&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pfsense.gateway.loss[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,/usr/local/bin/php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"require '/etc/inc/util.inc'; echo get_gateway_loss('&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pfsense.dhcp.leases[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,/usr/local/bin/php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"require '/etc/inc/util.inc'; echo count_dhcp_leases('&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pfsense.firmware.version,/usr/local/bin/php &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"require '/etc/inc/util.inc'; echo get_firmware_version();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Building Grafana Dashboards
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dashboard 1: Infrastructure Overview
&lt;/h3&gt;

&lt;p&gt;The single pane of glass for your entire homelab:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panels:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Row: "Host Status" ────────────────────────────────
  [Stat]  Hosts Up          zabbix: hosts.count{status=0}
  [Stat]  Hosts Down        zabbix: hosts.count{status=1}
  [Stat]  Active Triggers   zabbix: triggers.count{value=1}

Row: "System Health" ──────────────────────────────
  [Time Series]  CPU Usage per Host    zabbix: system.cpu.util{host=*}
  [Gauge]        Memory % per Host     zabbix: vm.memory.util{host=*}
  [Time Series]  Disk I/O per Host     zabbix: vfs.dev.read{host=*}, vfs.dev.write{host=*}

Row: "Network" ─────────────────────────────────────
  [Time Series]  Traffic per VLAN      zabbix: net.if.in{host=pfsense,if=VLAN*}
  [Stat]         Firewall Denials/h    zabbix: pf.deny.count
  [Table]        Top Talkers          zabbix: net.if.total{host=*}

Row: "Storage" ─────────────────────────────────────
  [Gauge]   Proxmox Storage Used  zabbix: pve.storage.used[*]
  [Bar]     Docker Disk Usage     zabbix: vfs.fs.size{host=docker*,fs=/var/lib/docker}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dashboard 2: pfSense Network Security
&lt;/h3&gt;

&lt;p&gt;Dedicated to monitoring the zero-trust firewall:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panels:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Row: "Firewall Activity" ──────────────────────────
  [Time Series]  Denials per VLAN/hour    zabbix: pf.deny{vlan=*}
  [Table]        Top Denied Sources       zabbix: pf.deny.src{groupby=src_ip}
  [Time Series]  Allow vs Deny Ratio      zabbix: pf.allow / pf.deny

Row: "Gateway Quality" ────────────────────────────
  [Time Series]  Packet Loss %             zabbix: pfsense.gateway.loss[*]
  [Time Series]  Latency ms               zabbix: pfsense.gateway.latency[*]
  [Stat]         Gateway Status            zabbix: pfsense.gateway.status

Row: "DHCP Leases" ────────────────────────────────
  [Stat]     MGMT Leases       zabbix: pfsense.dhcp.leases[MGMT]
  [Stat]     Server Leases     zabbix: pfsense.dhcp.leases[SERVERS]
  [Stat]     IoT Leases        zabbix: pfsense.dhcp.leases[IOT]
  [Stat]     Guest Leases      zabbix: pfsense.dhcp.leases[GUEST]
  [Table]    New Leases (24h)   zabbix: pfsense.dhcp.new_leases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dashboard 3: Proxmox Virtualization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Panels:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Row: "Cluster Health" ─────────────────────────────
  [Stat]    Cluster Status          zabbix: pve.cluster.status
  [Stat]    VMs Running             zabbix: pve.vm.count
  [Stat]    CTs Running             zabbix: pve.ct.count

Row: "Resource Usage" ─────────────────────────────
  [Gauge]    CPU Total              zabbix: system.cpu.util{host=proxmox*}
  [Gauge]    Memory Total           zabbix: vm.memory.util{host=proxmox*}
  [Bar]      Storage per Pool      zabbix: pve.storage.used[*] / pve.storage.total[*]

Row: "VM/CT Details" ─────────────────────────────
  [Table]    All VMs + CPU/Mem/Disk  zabbix: pve.vm.{cpu,mem,disk}[*]
  [Table]    All CTs + CPU/Mem/Disk  zabbix: pve.ct.{cpu,mem,disk}[*]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Grafana Variables for Reusable Dashboards
&lt;/h3&gt;

&lt;p&gt;Set up template variables so dashboards work across all hosts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Variable: $host
  Type: Query
  Query: zabbix: hosts*
  Multi-value: Yes
  Include All: Yes

Variable: $vlan
  Type: Custom
  Values: MGMT, SERVERS, IOT, GUEST, SERVICES

Variable: $interval
  Type: Interval
  Values: 1m,5m,10m,30m,1h,6h,1d
  Auto: Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Alerting: Wake Me When It Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zabbix Triggers → Grafana Alerts
&lt;/h3&gt;

&lt;p&gt;The alerting pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zabbix Agent → Zabbix Server (trigger fires) → Grafana Alert Rule → Notification Policy → Channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Critical Alerts (Wake Me Up)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Trigger Expression&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host down&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nodata(5m)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disaster&lt;/td&gt;
&lt;td&gt;Telegram + Email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk &amp;gt; 90%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last(/{HOST}/vfs.fs.size[pct])&amp;gt;90&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pfSense down&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nodata(3m)&lt;/code&gt; on pfSense&lt;/td&gt;
&lt;td&gt;Disaster&lt;/td&gt;
&lt;td&gt;Telegram + Email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway packet loss &amp;gt; 10%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last(/{HOST}/pfsense.gateway.loss)&amp;gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zabbix server down&lt;/td&gt;
&lt;td&gt;Internal zabbix trigger&lt;/td&gt;
&lt;td&gt;Disaster&lt;/td&gt;
&lt;td&gt;Email (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Warning Alerts (Check in Morning)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Trigger Expression&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU &amp;gt; 80% sustained&lt;/td&gt;
&lt;td&gt;&lt;code&gt;avg(10m)&amp;gt;80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Dashboard only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory &amp;gt; 85%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last(/{HOST}/vm.memory.util)&amp;gt;85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Dashboard only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate expiring &amp;lt; 14 days&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last(/{HOST}/cert.days_left)&amp;lt;14&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Email digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker container stopped&lt;/td&gt;
&lt;td&gt;&lt;code&gt;last(/{HOST}/docker.container.running)&amp;lt;expected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Dashboard only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Information Alerts (Weekly Digest)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Trigger Expression&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New DHCP lease on MGMT VLAN&lt;/td&gt;
&lt;td&gt;Event log match&lt;/td&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Weekly digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firmware update available&lt;/td&gt;
&lt;td&gt;&lt;code&gt;diff(/{HOST}/pfsense.firmware.version)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Weekly digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage growth rate &amp;gt; 5%/week&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trend(7d)&amp;gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Weekly digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Telegram Alert Integration
&lt;/h3&gt;

&lt;p&gt;Grafana supports Telegram natively. Create a bot via &lt;a href="https://t.me/BotFather" rel="noopener noreferrer"&gt;@BotFather&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;Grafana → Alerting → Contact points → Add Contact Point&lt;/span&gt;
  &lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Telegram&lt;/span&gt;
  &lt;span class="s"&gt;BOT API Token&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_BOT_TOKEN&lt;/span&gt;
  &lt;span class="s"&gt;Chat ID&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_CHAT_ID&lt;/span&gt;

&lt;span class="na"&gt;Notification Policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Group by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alertname, severity&lt;/span&gt;
  &lt;span class="na"&gt;Group wait&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
  &lt;span class="na"&gt;Group interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
  &lt;span class="na"&gt;Repeat interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4h&lt;/span&gt;

  &lt;span class="na"&gt;Route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;severity=disaster → Telegram immediately&lt;/span&gt;
  &lt;span class="na"&gt;Route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;severity=high → Telegram, 5m repeat&lt;/span&gt;
  &lt;span class="na"&gt;Route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;severity=warning → Email digest, 1d repeat&lt;/span&gt;
  &lt;span class="na"&gt;Route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;severity=info → Weekly email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Tuning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zabbix Housekeeper
&lt;/h3&gt;

&lt;p&gt;Zabbix's built-in housekeeper is notoriously slow with PostgreSQL. Replace it with partitioned tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Connect to Zabbix database&lt;/span&gt;
&lt;span class="n"&gt;sudo&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="n"&gt;zabbix&lt;/span&gt;

&lt;span class="c1"&gt;-- Enable partitioning extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Convert history tables to hypertables (TimescaleDB)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history_uint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history_str'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'trends'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'trends_uint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_time_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Disable Zabbix internal housekeeper&lt;/strong&gt; (TimescaleDB handles it now):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/zabbix/zabbix_server.conf
&lt;/span&gt;&lt;span class="n"&gt;DisableHousekeeping&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set retention policies:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Keep raw history for 14 days&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history_uint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'history_str'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'14 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Keep trends for 2 years&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'trends'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 years'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'trends_uint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'2 years'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Database Size Estimates
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monitoring&lt;/th&gt;
&lt;th&gt;Items&lt;/th&gt;
&lt;th&gt;History/Day&lt;/th&gt;
&lt;th&gt;14-Day History&lt;/th&gt;
&lt;th&gt;2-Year Trends&lt;/th&gt;
&lt;th&gt;Total DB Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 hosts&lt;/td&gt;
&lt;td&gt;~500&lt;/td&gt;
&lt;td&gt;~15 MB&lt;/td&gt;
&lt;td&gt;~210 MB&lt;/td&gt;
&lt;td&gt;~200 MB&lt;/td&gt;
&lt;td&gt;~500 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 hosts&lt;/td&gt;
&lt;td&gt;~1000&lt;/td&gt;
&lt;td&gt;~30 MB&lt;/td&gt;
&lt;td&gt;~420 MB&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;td&gt;~1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 hosts&lt;/td&gt;
&lt;td&gt;~2000&lt;/td&gt;
&lt;td&gt;~60 MB&lt;/td&gt;
&lt;td&gt;~840 MB&lt;/td&gt;
&lt;td&gt;~800 MB&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A homelab with 10-20 hosts will use 1-2 GB of storage over 2 years. That's nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backup Strategy
&lt;/h2&gt;

&lt;p&gt;Your monitoring data is valuable — it contains your baseline, your history, your incident timeline. Back it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zabbix Database Backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# zabbix-backup.sh — Daily Zabbix database backup&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/nas/backups/zabbix"&lt;/span&gt;
&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;RETENTION_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30

&lt;span class="c"&gt;# PostgreSQL dump&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg_dump zabbix | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/zabbix_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz"&lt;/span&gt;

&lt;span class="c"&gt;# Zabbix config&lt;/span&gt;
&lt;span class="nb"&gt;tar &lt;/span&gt;czf &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/zabbix_config_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/zabbix/ /etc/grafana/

&lt;span class="c"&gt;# Cleanup old backups&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"zabbix_*.sql.gz"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RETENTION_DAYS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-delete&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"zabbix_config_*.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; +&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RETENTION_DAYS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-delete&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup complete: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Grafana Dashboard Export
&lt;/h3&gt;

&lt;p&gt;Grafana dashboards should be version-controlled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# grafana-export.sh — Export all dashboards as JSON&lt;/span&gt;
&lt;span class="nv"&gt;GRAFANA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://10.0.20.11:3000"&lt;/span&gt;
&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_GRAFANA_API_KEY"&lt;/span&gt;
&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/commstech/grafana-dashboards"&lt;/span&gt;

&lt;span class="c"&gt;# Get all dashboard UIDs&lt;/span&gt;
&lt;span class="nv"&gt;DASHBOARDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/search?type=dash-db"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | .uid'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Export each dashboard&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;UID &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DASHBOARDS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GRAFANA_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/dashboards/uid/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
    jq &lt;span class="s1"&gt;'.dashboard'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OUTPUT_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Exported &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DASHBOARDS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; dashboards"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit these to git. Your dashboards are code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zabbix Server (LXC on Proxmox)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Already have hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana (LXC on Proxmox)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Already have hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL + TimescaleDB&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zabbix Agents&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage (2 GB over 2 years)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Negligible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram bot for alerts&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Monthly Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-hosted, zero subscriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare to Datadog at $15/host/month for 10 hosts = $150/month = $1,800/year. You're saving $1,800/year by self-hosting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dashboard Screenshots Description
&lt;/h2&gt;

&lt;p&gt;Since this is a text article, here's what your dashboards should look like:&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure Overview Dashboard
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top row:&lt;/strong&gt; Three large stat panels — green "5 Hosts Up", red "0 Hosts Down", orange "2 Active Warnings"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle left:&lt;/strong&gt; Time series graph showing CPU usage for all hosts over last 1 hour, with 80% threshold line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle right:&lt;/strong&gt; Gauge panels showing memory usage per host (color-coded: green &amp;lt; 60%, yellow 60-80%, red &amp;gt; 80%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom left:&lt;/strong&gt; Network traffic stacked area chart per VLAN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom right:&lt;/strong&gt; Storage usage horizontal bar chart per Proxmox pool&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  pfSense Security Dashboard
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top row:&lt;/strong&gt; Firewall deny rate time series — should show consistent low rate, any spike is suspicious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle:&lt;/strong&gt; Table of top 10 denied source IPs with last attempt time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom:&lt;/strong&gt; DHCP lease count per VLAN as small bar charts, with "new in 24h" annotation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;With monitoring in place, you can now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up automated remediation&lt;/strong&gt; — Zabbix can run scripts on alert (restart a service, clear a cache)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add log monitoring&lt;/strong&gt; — Forward syslog from pfSense, Proxmox, and Docker to Zabbix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement capacity planning&lt;/strong&gt; — Use trend data to predict when you'll run out of disk/CPU/memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add synthetic monitoring&lt;/strong&gt; — Zabbix web scenarios to check your services are actually responding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate with Home Assistant&lt;/strong&gt; — Send Zabbix alerts to your home automation for visual/audio alerts&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted monitoring costs nothing but electricity&lt;/strong&gt; — Zabbix + Grafana is enterprise-grade, free, and yours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zabbix for collection, Grafana for visualization&lt;/strong&gt; — each tool does what it does best&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active checks work across VLANs&lt;/strong&gt; — agents push data, no need to open inbound ports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune your triggers for homelab scale&lt;/strong&gt; — enterprise defaults are too sensitive for home infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimescaleDB partitioning is essential&lt;/strong&gt; — the built-in housekeeper will kill your database performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on what matters, ignore what doesn't&lt;/strong&gt; — disaster = wake me, warning = check in morning, info = weekly digest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-control your dashboards&lt;/strong&gt; — they're infrastructure code, not click-and-hope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back up your monitoring data&lt;/strong&gt; — it's your operational history, and losing it means losing your baselines&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;CommsNet — Building infrastructure that respects your privacy and your intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow on &lt;a href="https://medium.com/@commsnet" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://dev.to/commsnet"&gt;Dev.to&lt;/a&gt; for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>infrastructure</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Zero-Trust Homelab: Network Segmentation with VLANs and pfSense</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:51:19 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/zero-trust-homelab-network-segmentation-with-vlans-and-pfsense-31ni</link>
      <guid>https://dev.to/lunacommsnet/zero-trust-homelab-network-segmentation-with-vlans-and-pfsense-31ni</guid>
      <description>&lt;h1&gt;
  
  
  Zero-Trust Homelab: Network Segmentation with VLANs and pfSense
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Published: June 15, 2026 | CommsNet&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If everything on your network can reach everything else, you don't have a network — you have a single point of failure the size of your entire infrastructure. One compromised IoT lightbulb becomes a pivot to your NAS. One breached container becomes a foothold on your management VLAN. In a flat network, trust is binary: you're either in, or you're out.&lt;/p&gt;

&lt;p&gt;Zero-trust networking changes the assumption. Nothing is trusted by default. Every segment, every device, every flow must prove it belongs. And the mechanism that enforces this at the homelab scale isn't some expensive SDN controller — it's VLANs on a managed switch, enforced by pfSense firewall rules.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through designing and implementing a zero-trust network segmentation strategy using IEEE 802.1Q VLANs, a managed switch, and pfSense as the inter-VLAN router and policy enforcement point. By the end, your IoT devices won't be able to touch your servers, your guest WiFi will be isolated at L2, and your management interfaces will require explicit allow rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Zero-Trust for a Homelab?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Flat Network Problem
&lt;/h3&gt;

&lt;p&gt;Most homelabs start as flat networks — one &lt;code&gt;/24&lt;/code&gt;, everything plugged into one switch, maybe a consumer router doing NAT. It works until it doesn't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack Vector&lt;/th&gt;
&lt;th&gt;Flat Network Impact&lt;/th&gt;
&lt;th&gt;Segmented Network Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compromised IoT device&lt;/td&gt;
&lt;td&gt;Lateral movement to NAS, servers&lt;/td&gt;
&lt;td&gt;Contained to IoT VLAN, blocked at firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malware on guest laptop&lt;/td&gt;
&lt;td&gt;Access to all L2 hosts&lt;/td&gt;
&lt;td&gt;Isolated to guest VLAN, no cross-VLAN routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposed service exploit&lt;/td&gt;
&lt;td&gt;Direct path to management interfaces&lt;/td&gt;
&lt;td&gt;Must traverse firewall rules between VLANs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS poisoning on one host&lt;/td&gt;
&lt;td&gt;Affects entire broadcast domain&lt;/td&gt;
&lt;td&gt;Limited to single VLAN scope&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Zero-Trust Principles (Adapted for Homelab)
&lt;/h3&gt;

&lt;p&gt;Enterprise zero-trust frameworks (NIST SP 800-207, Forrester ZTX) assume things homelabbers don't have — identity providers, microsegmentation platforms, continuous verification. But the core principles translate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust, always verify&lt;/strong&gt; — Every inter-VLAN flow is denied by default, explicitly allowed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least privilege access&lt;/strong&gt; — VLANs get only the routing rules they need&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume breach&lt;/strong&gt; — Design so that compromising one VLAN doesn't compromise others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify explicitly&lt;/strong&gt; — pfSense rules are the policy; logging proves enforcement&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Network Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VLAN Architecture
&lt;/h3&gt;

&lt;p&gt;Here's the segmentation model I use. It's designed around the principle of isolation by trust level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    VLAN Architecture                      │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │  VLAN 10  │  │  VLAN 20  │  │  VLAN 30  │  │ VLAN 40 │ │
│  │Management│  │  Servers  │  │   IoT     │  │  Guest  │ │
│  │10.0.10/24│  │10.0.20/24│  │10.0.30/24│  │10.0.40 │ │
│  │ TRUST: H │  │ TRUST: H │  │ TRUST: L │  │TRUST: N│ │
│  └─────┬────┘  └─────┬────┘  └─────┬────┘  └────┬────┘ │
│        │              │              │            │       │
│        └──────────────┴──────┬───────┴────────────┘       │
│                              │                            │
│                     ┌────────┴────────┐                   │
│                     │    pfSense      │                   │
│                     │  Inter-VLAN     │                   │
│                     │  Router + FW    │                   │
│                     └────────────────┘                   │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  VLAN Definitions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VLAN ID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Subnet&lt;/th&gt;
&lt;th&gt;Trust Level&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Management&lt;/td&gt;
&lt;td&gt;10.0.10.0/24&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;pfSense, Proxmox, switches, IPMI/iDRAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Servers&lt;/td&gt;
&lt;td&gt;10.0.20.0/24&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;NAS, Docker hosts, application servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;IoT&lt;/td&gt;
&lt;td&gt;10.0.30.0/24&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Smart devices, sensors, cameras&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Guest&lt;/td&gt;
&lt;td&gt;10.0.40.0/24&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Guest WiFi, untrusted devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Services&lt;/td&gt;
&lt;td&gt;10.0.50.0/24&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Exposed services, reverse proxy, tunnel endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Trust Model and Flow Rules
&lt;/h3&gt;

&lt;p&gt;The firewall policy is the heart of zero-trust. Every rule is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source VLAN → Destination VLAN → Allowed? → Reason
─────────────────────────────────────────────────────
Management → Any              → ALLOW     → Admin access
Servers    → Management       → DENY      → No reverse access to mgmt
Servers    → Servers          → ALLOW     → Internal service communication
Servers    → IoT              → DENY      → No need to reach IoT
Servers    → Services         → ALLOW     → Publish services
IoT       → Servers           → LIMITED   → Only specific ports (DNS, NTP, specific API)
IoT       → Management       → DENY      → No access to mgmt interfaces
IoT       → Internet         → ALLOW     → Firmware updates, cloud APIs
IoT       → Guest            → DENY      → Mutual isolation
Guest     → Any (internal)   → DENY      → Complete isolation
Guest     → Internet          → ALLOW     → Internet-only access
Services  → Servers           → ALLOW     → Reverse proxy to backends
Services  → Management       → DENY      — No access to mgmt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Hardware Requirements
&lt;/h2&gt;

&lt;p&gt;You don't need enterprise gear. Here's what works at the homelab scale:&lt;/p&gt;

&lt;h3&gt;
  
  
  Managed Switch (Required)
&lt;/h3&gt;

&lt;p&gt;You need a switch that supports 802.1Q VLAN tagging. Options by budget:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Switch&lt;/th&gt;
&lt;th&gt;Ports&lt;/th&gt;
&lt;th&gt;VLAN Support&lt;/th&gt;
&lt;th&gt;Price (Used)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cisco Catalyst 2960&lt;/td&gt;
&lt;td&gt;24/48&lt;/td&gt;
&lt;td&gt;Full 802.1Q&lt;/td&gt;
&lt;td&gt;$30-80&lt;/td&gt;
&lt;td&gt;Bulletproof, CLI only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPE ProCurve 2530&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Full 802.1Q&lt;/td&gt;
&lt;td&gt;$40-100&lt;/td&gt;
&lt;td&gt;Web UI + CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TP-Link SG108E&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Basic 802.1Q&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;Budget starter, limited CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubiquiti USW-24&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Full + UI&lt;/td&gt;
&lt;td&gt;$100-200&lt;/td&gt;
&lt;td&gt;UniFi ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MikroTik CRS326&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;Full + RouterOS&lt;/td&gt;
&lt;td&gt;$80&lt;/td&gt;
&lt;td&gt;Powerful but steep learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;My pick:&lt;/strong&gt; Cisco Catalyst 2960G — cheap on eBay, never dies, full VLAN feature set. The CLI is an investment that pays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  pfSense Hardware
&lt;/h3&gt;

&lt;p&gt;pfSense runs the inter-VLAN routing and firewall. Options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Performance&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Netgate SG-1100&lt;/td&gt;
&lt;td&gt;$179&lt;/td&gt;
&lt;td&gt;~1 Gbps routing&lt;/td&gt;
&lt;td&gt;Purpose-built, low power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protectli Vault&lt;/td&gt;
&lt;td&gt;$300+&lt;/td&gt;
&lt;td&gt;~2 Gbps routing&lt;/td&gt;
&lt;td&gt;Intel NICs, fanless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repurposed PC + 4 NICs&lt;/td&gt;
&lt;td&gt;$50-100&lt;/td&gt;
&lt;td&gt;3+ Gbps routing&lt;/td&gt;
&lt;td&gt;Best performance/$, more power draw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxmox VM + virtio NICs&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;~10 Gbps routing&lt;/td&gt;
&lt;td&gt;Already have Proxmox? Use it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;My pick:&lt;/strong&gt; pfSense as a Proxmox VM with 4 virtio NICs. You already have the hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation: Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Configure VLANs on pfSense
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Interfaces → Assignments → VLANs&lt;/strong&gt; and create each VLAN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parent Interface: vmx0 (LAN)
VLAN Tag: 10  Priority: 0  Description: Management
VLAN Tag: 20  Priority: 0  Description: Servers  
VLAN Tag: 30  Priority: 0  Description: IoT
VLAN Tag: 40  Priority: 0  Description: Guest
VLAN Tag: 50  Priority: 0  Description: Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then assign each VLAN as a new interface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interfaces → Assignments:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vmx0.10&lt;/code&gt; → OPT1 → rename to &lt;code&gt;MGMT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmx0.20&lt;/code&gt; → OPT2 → rename to &lt;code&gt;SERVERS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmx0.30&lt;/code&gt; → OPT3 → rename to &lt;code&gt;IOT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmx0.40&lt;/code&gt; → OPT4 → rename to &lt;code&gt;GUEST&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vmx0.50&lt;/code&gt; → OPT5 → rename to &lt;code&gt;SERVICES&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enable each interface and set its IPv4 Static address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MGMT:     10.0.10.1/24
SERVERS:  10.0.20.1/24
IOT:      10.0.30.1/24
GUEST:    10.0.40.1/24
SERVICES: 10.0.50.1/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Configure DHCP for Each VLAN
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Services → DHCP Server&lt;/strong&gt; — enable on each VLAN interface:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;DNS&lt;/th&gt;
&lt;th&gt;Gateway&lt;/th&gt;
&lt;th&gt;Lease Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MGMT&lt;/td&gt;
&lt;td&gt;10.0.10.100-10.0.10.200&lt;/td&gt;
&lt;td&gt;10.0.10.1&lt;/td&gt;
&lt;td&gt;10.0.10.1&lt;/td&gt;
&lt;td&gt;86400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERVERS&lt;/td&gt;
&lt;td&gt;10.0.20.100-10.0.20.200&lt;/td&gt;
&lt;td&gt;10.0.20.1&lt;/td&gt;
&lt;td&gt;10.0.20.1&lt;/td&gt;
&lt;td&gt;86400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IOT&lt;/td&gt;
&lt;td&gt;10.0.30.50-10.0.30.200&lt;/td&gt;
&lt;td&gt;1.1.1.1, 9.9.9.9&lt;/td&gt;
&lt;td&gt;10.0.30.1&lt;/td&gt;
&lt;td&gt;3600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GUEST&lt;/td&gt;
&lt;td&gt;10.0.40.50-10.0.40.200&lt;/td&gt;
&lt;td&gt;1.1.1.1&lt;/td&gt;
&lt;td&gt;10.0.40.1&lt;/td&gt;
&lt;td&gt;3600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERVICES&lt;/td&gt;
&lt;td&gt;Static only&lt;/td&gt;
&lt;td&gt;10.0.50.1&lt;/td&gt;
&lt;td&gt;10.0.50.1&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IoT/Guest DNS:&lt;/strong&gt; Point to public resolvers (1.1.1.1), NOT your internal DNS. These VLANs don't need to resolve internal names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services:&lt;/strong&gt; No DHCP — all services get static IPs. If someone plugs into this VLAN, they get nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short lease times on IoT/Guest:&lt;/strong&gt; Limits how long a rogue device keeps an address.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Configure the Managed Switch
&lt;/h3&gt;

&lt;p&gt;This is where VLAN theory meets physical reality. Each switch port must be configured as &lt;strong&gt;access&lt;/strong&gt; (untagged, single VLAN) or &lt;strong&gt;trunk&lt;/strong&gt; (tagged, multiple VLANs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port assignments:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Port 1-8:   Access, VLAN 20 (Servers)      — NAS, Proxmox hosts, Docker servers
Port 9-12:  Access, VLAN 30 (IoT)          — Smart devices, cameras
Port 13-16: Access, VLAN 40 (Guest)        — Guest WiFi AP
Port 17-20: Access, VLAN 10 (Management)   — Management workstations
Port 21-24: Trunk, VLAN ALL (tagged)        — pfSense, Proxmox, WiFi AP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cisco 2960 example config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;! Trunk port to pfSense
interface GigabitEthernet0/21
  switchport mode trunk
  switchport trunk allowed vlan 10,20,30,40,50
  switchport trunk native vlan 99  ! Never use VLAN 1 as native
  spanning-tree portfast trunk

! Access port for server
interface range GigabitEthernet0/1 - 8
  switchport mode access
  switchport access vlan 20
  spanning-tree portfast

! Access port for IoT
interface range GigabitEthernet0/9 - 12
  switchport mode access
  switchport access vlan 30
  spanning-tree portfast

! Access port for management workstation
interface range GigabitEthernet0/17 - 20
  switchport mode access
  switchport access vlan 10
  spanning-tree portfast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; Set &lt;code&gt;native vlan 99&lt;/code&gt; (or any unused VLAN) on trunk ports. Never use VLAN 1 as the native VLAN — it's the default attack surface on every Cisco switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Firewall Rules — The Zero-Trust Core
&lt;/h3&gt;

&lt;p&gt;This is where zero-trust is actually enforced. Navigate to &lt;strong&gt;Firewall → Rules&lt;/strong&gt; for each interface.&lt;/p&gt;

&lt;h4&gt;
  
  
  Management VLAN (Highest Trust)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# MGMT Interface Rules (evaluated top-to-bottom)&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 1. Allow MGMT to anything (admin access)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  MGMT net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"MGMT: Allow all outbound"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2. Allow pfSense DNS from MGMT&lt;/span&gt;
IPv4 TCP/UDP  MGMT net  &lt;span class="k"&gt;*&lt;/span&gt;  MGMT address  53  →  ALLOW  — &lt;span class="s2"&gt;"MGMT: DNS to pfSense"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 3. Block everything else (implicit, but be explicit)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  MGMT net  &lt;span class="k"&gt;*&lt;/span&gt;  →  DENY  — &lt;span class="s2"&gt;"MGMT: Block all inbound"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Management is simple — full outbound access, no inbound from other VLANs. Admins initiate connections to infrastructure; infrastructure never reaches back.&lt;/p&gt;

&lt;h4&gt;
  
  
  Servers VLAN
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SERVERS Interface Rules&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 1. Allow Servers → Servers (internal communication)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"SERVERS: Inter-server communication"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2. Allow Servers → Internet (updates, API calls)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="o"&gt;!&lt;/span&gt;RFC1918  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"SERVERS: Internet access"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 3. Allow Services → Servers (reverse proxy backends)&lt;/span&gt;
IPv4 TCP  SERVICES net  &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  80,443,8080  →  ALLOW  — &lt;span class="s2"&gt;"SERVICES: Reverse proxy to backends"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 4. Block everything else&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  &lt;span class="k"&gt;*&lt;/span&gt;  →  DENY  — &lt;span class="s2"&gt;"SERVERS: Block all other inbound"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key rule:&lt;/strong&gt; &lt;code&gt;SERVERS → !RFC1918&lt;/code&gt; allows internet access but blocks access to any other internal VLAN. Servers can reach the internet for updates but cannot reach IoT, Guest, or Management.&lt;/p&gt;

&lt;h4&gt;
  
  
  IoT VLAN (Low Trust)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# IOT Interface Rules — MOST RESTRICTIVE&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 1. Allow IoT → pfSense DNS&lt;/span&gt;
IPv4 TCP/UDP  IOT net  &lt;span class="k"&gt;*&lt;/span&gt;  IOT address  53  →  ALLOW  — &lt;span class="s2"&gt;"IOT: DNS to pfSense"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2. Allow IoT → pfSense NTP&lt;/span&gt;
IPv4 UDP  IOT net  &lt;span class="k"&gt;*&lt;/span&gt;  IOT address  123  →  ALLOW  — &lt;span class="s2"&gt;"IOT: NTP to pfSense"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 3. Allow specific IoT → Server API (e.g., Home Assistant)&lt;/span&gt;
IPv4 TCP  IOT net  &lt;span class="k"&gt;*&lt;/span&gt;  10.0.20.50  8123  →  ALLOW  — &lt;span class="s2"&gt;"IOT: Home Assistant API"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 4. Allow IoT → Internet (firmware, cloud APIs)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  IOT net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="o"&gt;!&lt;/span&gt;RFC1918  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"IOT: Internet access"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 5. Block everything else&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  IOT net  &lt;span class="k"&gt;*&lt;/span&gt;  →  DENY  — &lt;span class="s2"&gt;"IOT: Block all other inbound"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IoT rules are surgical — only DNS, NTP, one specific API endpoint, and internet. This is the most important VLAN to lock down because IoT devices are the most likely to be compromised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Use aliases in pfSense for the IoT API rules. Create an alias &lt;code&gt;IoT_Allowed_Services&lt;/code&gt; with the specific server IPs and ports IoT devices need. This makes it easy to add new devices without creating new rules.&lt;/p&gt;

&lt;h4&gt;
  
  
  Guest VLAN (No Trust)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GUEST Interface Rules — TOTAL ISOLATION&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 1. Allow Guest → Internet only&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  GUEST net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="o"&gt;!&lt;/span&gt;RFC1918  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"GUEST: Internet only"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2. Allow Guest → pfSense DNS (DHCP-assigned)&lt;/span&gt;
IPv4 TCP/UDP  GUEST net  &lt;span class="k"&gt;*&lt;/span&gt;  GUEST address  53  →  ALLOW  — &lt;span class="s2"&gt;"GUEST: DNS"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 3. Block everything else&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  GUEST net  &lt;span class="k"&gt;*&lt;/span&gt;  →  DENY  — &lt;span class="s2"&gt;"GUEST: Complete isolation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guests get internet. Nothing else. This is the simplest and most important rule set.&lt;/p&gt;

&lt;h4&gt;
  
  
  Services VLAN (Exposed Services)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SERVICES Interface Rules&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 1. Allow Services → Servers (reverse proxy backends)&lt;/span&gt;
IPv4 TCP  SERVICES net  &lt;span class="k"&gt;*&lt;/span&gt;  SERVERS net  80,443,8080,8123  →  ALLOW  — &lt;span class="s2"&gt;"SERVICES: Proxy to backends"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 2. Allow Services → Internet (cert renewal, API calls)&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  SERVICES net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="o"&gt;!&lt;/span&gt;RFC1918  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  — &lt;span class="s2"&gt;"SERVICES: Internet access"&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# 3. Block everything else&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  SERVICES net  &lt;span class="k"&gt;*&lt;/span&gt;  →  DENY  — &lt;span class="s2"&gt;"SERVICES: Block all other inbound"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Services VLAN is the DMZ equivalent. It can reach backends on specific ports and the internet for Let's Encrypt/certbot renewals. It cannot reach management, IoT, or guest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advanced: pfSense Aliases for Zero-Trust Maintenance
&lt;/h2&gt;

&lt;p&gt;One of the most underused features in pfSense is &lt;strong&gt;aliases&lt;/strong&gt; — named groups of networks, hosts, or ports that make firewall rules readable and maintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firewall → Aliases:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: MGMT_Net        Type: Host(s)    Value: 10.0.10.0/24
Name: SERVERS_Net     Type: Host(s)    Value: 10.0.20.0/24
Name: IOT_Net         Type: Host(s)    Value: 10.0.30.0/24
Name: GUEST_Net       Type: Host(s)    Value: 10.0.40.0/24
Name: SERVICES_Net    Type: Host(s)    Value: 10.0.50.0/24
Name: RFC1918         Type: Network   Value: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Name: IoT_API_Targets Type: Host(s)    Value: 10.0.20.50  (Home Assistant)
Name: Web_Ports       Type: Port(s)    Value: 80, 443, 8080
Name: IoT_Allowed     Type: Port(s)    Value: 8123, 1883, 5353
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your rules use human-readable names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DENY  IOT_Net    →  RFC1918       —  *        —  "IOT: Block internal access"
ALLOW IOT_Net    →  IoT_API_Targets → IoT_Allowed —  "IOT: Allowed services only"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you add a new IoT device that needs to reach a different service, you update the alias — not the rule. This prevents rule sprawl, which is the #1 way zero-trust degrades over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification: Proving Your Segmentation Works
&lt;/h2&gt;

&lt;p&gt;Setting up rules isn't enough. You need to verify they work. Here's how:&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1: IoT Cannot Reach Management
&lt;/h3&gt;

&lt;p&gt;From an IoT device (or a laptop plugged into an IoT port):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should FAIL — blocked by firewall&lt;/span&gt;
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 22 10.0.10.1    &lt;span class="c"&gt;# pfSense SSH&lt;/span&gt;
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 443 10.0.10.1   &lt;span class="c"&gt;# pfSense WebUI&lt;/span&gt;
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 8006 10.0.10.5  &lt;span class="c"&gt;# Proxmox WebUI&lt;/span&gt;

&lt;span class="c"&gt;# Should SUCCEED — allowed&lt;/span&gt;
ping 1.1.1.1            &lt;span class="c"&gt;# Internet&lt;/span&gt;
dig @10.0.30.1 example.com  &lt;span class="c"&gt;# DNS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 2: Guest Cannot Reach Any Internal Host
&lt;/h3&gt;

&lt;p&gt;From a guest device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# All should FAIL&lt;/span&gt;
ping 10.0.10.1    &lt;span class="c"&gt;# Management&lt;/span&gt;
ping 10.0.20.1    &lt;span class="c"&gt;# Servers&lt;/span&gt;
ping 10.0.30.1    &lt;span class="c"&gt;# IoT&lt;/span&gt;
nmap 10.0.20.0/24 &lt;span class="c"&gt;# Scan entire server VLAN&lt;/span&gt;

&lt;span class="c"&gt;# Should SUCCEED&lt;/span&gt;
ping 1.1.1.1      &lt;span class="c"&gt;# Internet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 3: Servers Cannot Reach Management
&lt;/h3&gt;

&lt;p&gt;From a server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should FAIL&lt;/span&gt;
ssh admin@10.0.10.1     &lt;span class="c"&gt;# pfSense&lt;/span&gt;
curl https://10.0.10.5:8006  &lt;span class="c"&gt;# Proxmox&lt;/span&gt;

&lt;span class="c"&gt;# Should SUCCEED&lt;/span&gt;
ping 10.0.20.1          &lt;span class="c"&gt;# Servers gateway&lt;/span&gt;
curl https://example.com &lt;span class="c"&gt;# Internet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 4: pfSense Firewall Logs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Status → System Logs → Firewall:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Watch for denied packets between VLANs. If you see unexpected allowed traffic, your rules are wrong. If you see denied traffic that should be allowed, check your alias definitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Enable &lt;strong&gt;Log packets that are handled by this rule&lt;/strong&gt; on every DENY rule. This gives you a complete audit trail of every attempted cross-VLAN connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  WiFi Considerations
&lt;/h2&gt;

&lt;p&gt;If you're running WiFi (and you should for IoT/mobile), the AP must support multiple SSIDs mapped to VLANs.&lt;/p&gt;

&lt;h3&gt;
  
  
  UniFi AP VLAN Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSID: "CommsNet-Home"     → VLAN 20 (Servers/Trusted devices)
SSID: "CommsNet-IoT"      → VLAN 30 (IoT only)
SSID: "CommsNet-Guest"    → VLAN 40 (Guest, rate-limited)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each SSID gets its own DHCP scope from pfSense, its own firewall rules, and complete L2 isolation. The AP tags frames with the appropriate VLAN ID; pfSense routes and enforces policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  WiFi-Specific Firewall Additions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rate limit Guest WiFi&lt;/span&gt;
IPv4 &lt;span class="k"&gt;*&lt;/span&gt;  GUEST net  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  &lt;span class="k"&gt;*&lt;/span&gt;  →  ALLOW  —  &lt;span class="s2"&gt;"GUEST: Rate limited"&lt;/span&gt;
  ↑ Limit: 10 Mbps down, 5 Mbps up per client
  ↑ Connection limit: 100 concurrent per /32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pfSense traffic shaping (limiter queues) can enforce per-client bandwidth caps on guest and IoT VLANs. This prevents one guest device from consuming your entire upstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring and Alerting
&lt;/h2&gt;

&lt;p&gt;Zero-trust without monitoring is just wishful thinking. You need to know when segmentation fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  pfSense Integration with Zabbix/Grafana
&lt;/h3&gt;

&lt;p&gt;Install the &lt;strong&gt;pfSense Zabbix Agent&lt;/strong&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Package Manager → Available Packages → pfSense-zabbix-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor these key metrics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What It Tells You&lt;/th&gt;
&lt;th&gt;Alert Threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firewall deny rate&lt;/td&gt;
&lt;td&gt;How many cross-VLAN attempts blocked&lt;/td&gt;
&lt;td&gt;Sudden spike = scanning/recon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface traffic per VLAN&lt;/td&gt;
&lt;td&gt;Normal traffic baseline&lt;/td&gt;
&lt;td&gt;IoT VLAN talking to server VLAN = breach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DHCP lease count per VLAN&lt;/td&gt;
&lt;td&gt;Device count per segment&lt;/td&gt;
&lt;td&gt;New device on Management VLAN = investigate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State table utilization&lt;/td&gt;
&lt;td&gt;Active connections&lt;/td&gt;
&lt;td&gt;&amp;gt;80% = possible flood or compromised host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pfSense CPU/memory&lt;/td&gt;
&lt;td&gt;Router health&lt;/td&gt;
&lt;td&gt;&amp;gt;80% sustained = capacity issue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Suricata IDS on pfSense
&lt;/h3&gt;

&lt;p&gt;Enable &lt;strong&gt;Suricata&lt;/strong&gt; on each VLAN interface for deep packet inspection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Services → Suricata → Interfaces → Add
  Interface: VLAN_30 (IoT)
  Mode: IPS (Inline Prevention)
  Block offenders: Yes
  Alert only on new threats: Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suricata on the IoT VLAN catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IoT devices phoning home to unexpected IPs&lt;/li&gt;
&lt;li&gt;Malware C2 beacon patterns&lt;/li&gt;
&lt;li&gt;Known exploit signatures targeting IoT firmware&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Mistakes and Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Forgetting to Disable VLAN 1
&lt;/h3&gt;

&lt;p&gt;VLAN 1 is the default on every switch. Every port is in it. If you don't explicitly disable it, you have a shadow flat network running alongside your segmented one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Create a &lt;code&gt;VLAN 99&lt;/code&gt; as the native VLAN on all trunk ports and move all unused ports to a dead VLAN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;! Move all unused ports to dead VLAN
interface range GigabitEthernet0/22 - 24
  switchport mode access
  switchport access vlan 99
  shutdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 2: Over-Allowing on Trunk Ports
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;switchport trunk allowed vlan all&lt;/code&gt; is lazy and dangerous. Only allow the VLANs that need to traverse each trunk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface GigabitEthernet0/21
  switchport trunk allowed vlan 10,20,30,40,50
  ! NOT: switchport trunk allowed vlan all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 3: pfSense Floating Rules Bypassing Interface Rules
&lt;/h3&gt;

&lt;p&gt;Floating rules in pfSense apply globally and can bypass per-interface rules. If you use floating rules, make sure they don't accidentally allow cross-VLAN traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Avoid floating rules for inter-VLAN policies. Use interface-specific rules. Use floating rules only for traffic shaping or QoS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: No Logging on Deny Rules
&lt;/h3&gt;

&lt;p&gt;If you don't log denied packets, you have no way to detect reconnaissance, failed intrusion attempts, or misconfigured devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Enable logging on every DENY rule. Set up a syslog server to aggregate and analyze firewall logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cisco 2960G (used)&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;eBay, 24-port managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pfSense (VM on Proxmox)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Already have hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cat6 patch cables&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;Monoprice 10-pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UniFi AP AC Lite (used)&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;td&gt;Multi-SSID VLAN support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$105&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-time cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No subscriptions. No per-device licensing. No cloud dependencies. This is infrastructure you own, understand, and control.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This article covers L2/L3 segmentation. In the next article in this series, we'll add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WireGuard VPN&lt;/strong&gt; for remote management VLAN access (never expose pfSense to the internet)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cilium eBPF policies&lt;/strong&gt; for micro-segmentation within the server VLAN (container-to-container)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pfSense CARP&lt;/strong&gt; for high-availability firewall failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated compliance checks&lt;/strong&gt; — scripts that verify your firewall rules match your zero-trust policy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flat networks are the enemy&lt;/strong&gt; — one compromised device becomes a pivot to everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLANs + pfSense rules = zero-trust at homelab scale&lt;/strong&gt; — no enterprise license required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny by default, allow by exception&lt;/strong&gt; — every inter-VLAN flow must be justified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IoT is your biggest risk&lt;/strong&gt; — isolate it to DNS + NTP + one API endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest gets internet only&lt;/strong&gt; — no exceptions, no "just this once"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every deny&lt;/strong&gt; — visibility is how you prove zero-trust works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aliases keep rules maintainable&lt;/strong&gt; — rule sprawl is how zero-trust dies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your segmentation&lt;/strong&gt; — if you haven't verified it, it's probably broken&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;CommsNet — Building infrastructure that respects your privacy and your intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow on &lt;a href="https://medium.com/@commsnet" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; and &lt;a href="https://dev.to/commsnet"&gt;Dev.to&lt;/a&gt; for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>networking</category>
      <category>security</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>From ISP Lock-in to Control: Self-Hosting Nextcloud/Home Assistant Behind Cloudflare Tunnels</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:51:00 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/from-isp-lock-in-to-control-self-hosting-nextcloudhome-assistant-behind-cloudflare-tunnels-50p7</link>
      <guid>https://dev.to/lunacommsnet/from-isp-lock-in-to-control-self-hosting-nextcloudhome-assistant-behind-cloudflare-tunnels-50p7</guid>
      <description>&lt;h1&gt;
  
  
  From ISP Lock-in to Control: Self-Hosting Nextcloud/Supervised Behind Cloudflare Tunnels
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Published: May 27, 2026 | CommsNet&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your ISP gave you a CGNAT address, blocked port 25, and thinks "advanced router settings" means changing the WiFi password. You're paying for a connection you don't control, and every cloud subscription is another monthly tax on data you should own.&lt;/p&gt;

&lt;p&gt;This article is the exit strategy. We're going to self-host &lt;strong&gt;Nextcloud&lt;/strong&gt; for file sync, calendar, and contacts — and &lt;strong&gt;Home Assistant Supervised&lt;/strong&gt; for home automation — all behind &lt;strong&gt;Cloudflare Tunnels&lt;/strong&gt; so nothing is exposed directly to the internet. No port forwarding, no DDNS, no static IP required. Your ISP doesn't even need to know you're running services.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with ISP Defaults
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What ISPs Do&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CGNAT (Carrier-Grade NAT)&lt;/td&gt;
&lt;td&gt;You can't receive inbound connections — no port forwarding possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocked ports (25, 80, 443)&lt;/td&gt;
&lt;td&gt;Even with a public IP, common services are blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic IP addresses&lt;/td&gt;
&lt;td&gt;DDNS works around this, but it's fragile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Residential TOS restrictions&lt;/td&gt;
&lt;td&gt;"No servers" clauses in fine print&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No IPv6 or broken IPv6&lt;/td&gt;
&lt;td&gt;Dual-stack isn't optional anymore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asymmetric bandwidth&lt;/td&gt;
&lt;td&gt;Upload is 10% of download — your "cloud" uploads crawl&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The answer isn't a VPS that adds another monthly bill and another provider who can suspend you. The answer is &lt;strong&gt;Cloudflare Tunnels&lt;/strong&gt; — outbound-only connections that punch through CGNAT, avoid blocked ports, and give you your own domain with TLS termination.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────┐
                    │  Cloudflare Edge  │
                    │  (Anycast CDN)    │
                    └────────┬─────────┘
                             │ HTTPS
                    ┌────────┴─────────┐
                    │ Cloudflare Tunnel  │
                    │ (cloudflared)      │
                    │ ← outbound only → │
                    └────────┬─────────┘
                             │ localhost
              ┌──────────────┼──────────────┐
              │              │              │
        ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
        │ Nextcloud  │ │  Home      │ │  Other     │
        │ :8080      │ │ Assistant  │ │ Services   │
        │            │ │  :8123     │ │  :XXXX     │
        └────────────┘ └───────────┘ └────────────┘
              │              │              │
        ┌─────┴─────────────┴──────────────┴────┐
        │            Docker Network              │
        │         (internal only)                │
        └───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; All connections are &lt;em&gt;outbound&lt;/em&gt; from your server to Cloudflare. Your firewall never opens an inbound port. Your ISP never sees a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Domain and Cloudflare Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A domain name (transfer to Cloudflare Registrar or use any registrar with Cloudflare DNS)&lt;/li&gt;
&lt;li&gt;A Cloudflare account (free tier works for tunnels)&lt;/li&gt;
&lt;li&gt;Docker and Docker Compose on your server&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  DNS Configuration
&lt;/h3&gt;

&lt;p&gt;Point your domain's nameservers to Cloudflare, then add A/AAAA records for your services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nextcloud.commsnet.org    → Proxied (orange cloud) → Tunnel
hass.commsnet.org         → Proxied (orange cloud) → Tunnel
files.commsnet.org        → Proxied (orange cloud) → Tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "Proxied" toggle is critical — it enables Cloudflare's TLS termination and DDoS protection. Your origin server IP never appears in DNS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Cloudflare Tunnel
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install cloudflared
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb &lt;span class="nt"&gt;-o&lt;/span&gt; cloudflared.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; cloudflared.deb

&lt;span class="c"&gt;# Or via package manager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cloudflared &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Authenticate and Create Tunnel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Authenticate with your Cloudflare account&lt;/span&gt;
cloudflared tunnel login

&lt;span class="c"&gt;# Create the tunnel&lt;/span&gt;
cloudflared tunnel create homelab

&lt;span class="c"&gt;# Note the tunnel ID from the output — you'll need it&lt;/span&gt;
&lt;span class="c"&gt;# Example: TUNNEL_ID=a1b2c3d4-e5f6-7890-abcd-ef1234567890&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure the Tunnel
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;~/.cloudflared/config.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tunnel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TUNNEL_ID&lt;/span&gt;
&lt;span class="na"&gt;credentials-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/youruser/.cloudflared/TUNNEL_ID.json&lt;/span&gt;

&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Nextcloud&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud.commsnet.org&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;
    &lt;span class="na"&gt;originRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;noTLSVerify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Home Assistant&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hass.commsnet.org&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8123&lt;/span&gt;
    &lt;span class="na"&gt;originRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;noTLSVerify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Catch-all rule (required)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_status:404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DNS Records
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create CNAME records pointing to the tunnel&lt;/span&gt;
cloudflared tunnel route dns homelab nextcloud.commsnet.org
cloudflared tunnel route dns homelab hass.commsnet.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically creates the CNAME records in Cloudflare DNS pointing &lt;code&gt;&amp;lt;subdomain&amp;gt;.commsnet.org&lt;/code&gt; to &lt;code&gt;&amp;lt;TUNNEL_ID&amp;gt;.cfargotunnel.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the Tunnel
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cloudflared tunnel run homelab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything works, you'll see the tunnel connect and your services will be accessible at their respective hostnames — all over HTTPS, all without opening a single inbound port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run as a Service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install as systemd service&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;cloudflared service &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Enable and start&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;cloudflared
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start cloudflared

&lt;span class="c"&gt;# Check status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Nextcloud with Docker Compose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nextcloud/
├── docker-compose.yml
├── .env
├── data/
│   ├── nextcloud/
│   ├── db/
│   └── redis/
└── config/
    ├── nginx/
    │   └── nginx.conf
    └── php/
        └── uploads.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nextcloud-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/db:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-internal&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;nextcloud-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-redis&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass ${REDIS_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-internal&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-a"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDIS_PASSWORD}"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;nextcloud-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:30-apache&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-app&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nextcloud-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;nextcloud-redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-db&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-redis&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_HOST_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${REDIS_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTCLOUD_ADMIN_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTCLOUD_ADMIN_USER}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTCLOUD_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTCLOUD_ADMIN_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTCLOUD_TRUSTED_DOMAINS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud.commsnet.org&lt;/span&gt;
      &lt;span class="na"&gt;OVERWRITEPROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;OVERWRITECLIURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://nextcloud.commsnet.org&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SMTP_HOST}&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;587&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_SECURE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SMTP_USER}&lt;/span&gt;
      &lt;span class="na"&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SMTP_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MAIL_FROM_ADDRESS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;MAIL_DOMAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;commsnet.org&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/nextcloud:/var/www/html&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-exposed&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-sf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:80/status.php&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;nextcloud-cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:30-apache&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-cron&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nextcloud-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/nextcloud:/var/www/html&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/cron.sh&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-internal&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nextcloud-internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# No external access — only app is exposed&lt;/span&gt;
  &lt;span class="na"&gt;nextcloud-exposed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environment File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env — NEVER commit this to git&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nextcloud
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nextcloud_db
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generate-with: openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&amp;gt;

&lt;span class="nv"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generate-with: openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&amp;gt;

&lt;span class="nv"&gt;NEXTCLOUD_ADMIN_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;span class="nv"&gt;NEXTCLOUD_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generate-with: openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&amp;gt;

&lt;span class="nv"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smtp.example.com
&lt;span class="nv"&gt;SMTP_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nextcloud@commsnet.org
&lt;span class="nv"&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-smtp-password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PHP Upload Limits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; config/php/uploads.ini
&lt;/span&gt;&lt;span class="py"&gt;upload_max_filesize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16G&lt;/span&gt;
&lt;span class="py"&gt;post_max_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;16G&lt;/span&gt;
&lt;span class="py"&gt;max_execution_time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;span class="py"&gt;max_input_time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;span class="py"&gt;memory_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Important: Internal Network Segmentation
&lt;/h3&gt;

&lt;p&gt;Notice the two Docker networks in the compose file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nextcloud-internal&lt;/code&gt;&lt;/strong&gt; — no external routing. Database and Redis can only be reached by Nextcloud containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nextcloud-exposed&lt;/code&gt;&lt;/strong&gt; — the app container sits on this network too, so Cloudflare Tunnel can reach it on &lt;code&gt;localhost:8080&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database and Redis are &lt;em&gt;never&lt;/em&gt; accessible from outside the Docker network. Even if an attacker compromises the tunnel, they can't directly reach PostgreSQL or Redis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Home Assistant Supervised
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Supervised, Not Home Assistant OS?
&lt;/h3&gt;

&lt;p&gt;Home Assistant OS is the easy path, but it's a black box — you can't install custom add-ons, manage it with Docker Compose, or integrate it with your existing infrastructure. Supervised gives you the same UI and add-on store, but on your terms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install required packages for Supervised&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  apparmor &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="se"&gt;\&lt;/span&gt;
  network-manager &lt;span class="se"&gt;\&lt;/span&gt;
  dbus &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="se"&gt;\&lt;/span&gt;
  socat &lt;span class="se"&gt;\&lt;/span&gt;
  avahi-daemon &lt;span class="se"&gt;\&lt;/span&gt;
  udisks2 &lt;span class="se"&gt;\&lt;/span&gt;
  libglib2.0-bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Docker Compose for Home Assistant
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;homeassistant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;homeassistant&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/home-assistant/home-assistant:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=America/Chicago&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/hass:/config&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;  &lt;span class="c1"&gt;# HA needs host networking for device discovery&lt;/span&gt;
    &lt;span class="c1"&gt;# Health check&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-sf"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8123"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="c1"&gt;# Home Assistant Supervised Installer&lt;/span&gt;
  &lt;span class="c1"&gt;# Run this ONCE to install the supervisor, then remove this service&lt;/span&gt;
  &lt;span class="na"&gt;hass-supervisor-installer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;homeassistant/amd64-hassio-supervisor:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hassio_supervisor&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SUPERVISOR_SHARE=/etc/hassio&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SUPERVISOR_NAME=hassio_supervisor&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/hassio:/etc/hassio&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/dbus:/var/run/dbus&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/machine-id:/etc/machine-id:ro&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Alternative: Simpler Home Assistant Container (Non-Supervised)
&lt;/h3&gt;

&lt;p&gt;If you don't need the add-on store, a plain HA container is simpler and more maintainable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;homeassistant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;homeassistant&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/home-assistant/home-assistant:stable&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TZ=America/Chicago&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/hass:/config&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8123:8123"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-sf"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8123"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homeassistant&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;homeassistant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cloudflare Tunnel Configuration for HA
&lt;/h3&gt;

&lt;p&gt;Add to your &lt;code&gt;~/.cloudflared/config.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# Home Assistant&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hass.commsnet.org&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://homeassistant:8123&lt;/span&gt;
    &lt;span class="na"&gt;originRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;noTLSVerify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Home Assistant requires WebSocket support for real-time updates. Cloudflare Tunnels handle WebSocket proxying automatically — no extra configuration needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Security Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Nextcloud Security
&lt;/h3&gt;

&lt;p&gt;After first login, configure these in &lt;code&gt;config.php&lt;/code&gt; (or via OCC):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// data/nextcloud/config/config.php additions&lt;/span&gt;

&lt;span class="s1"&gt;'force_ssl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'default_phone_region'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'memcache.local'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'\\OC\\Memcache\\APCu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'memcache.distributed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'\\OC\\Memcache\\Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'memcache.locking'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'\\OC\\Memcache\\Redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'redis'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'nextcloud-redis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'your-redis-password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="s1"&gt;'maintenance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'theme'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'loglevel'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'auth.bruteforce.protection.enabled'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'share_folder'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/Shared'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'trashbin_retention_obligation'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'30,180'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'activity_expire_days'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s1"&gt;'simpleSignUpLink.shown'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fail2Ban Integration (Optional but Recommended)
&lt;/h3&gt;

&lt;p&gt;If someone tries to brute-force your Nextcloud login, fail2ban will ban their IP at the firewall level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/fail2ban/filter.d/nextcloud.conf
&lt;/span&gt;&lt;span class="nn"&gt;[Definition]&lt;/span&gt;
&lt;span class="py"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
&lt;span class="py"&gt;failregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;^.*Login failed.*Remote IP.*&amp;lt;HOST&amp;gt;.*$&lt;/span&gt;
            &lt;span class="err"&gt;^.*Bruteforce&lt;/span&gt; &lt;span class="err"&gt;attempt.*Remote&lt;/span&gt; &lt;span class="err"&gt;IP.*&amp;lt;HOST&amp;gt;.*$&lt;/span&gt;
&lt;span class="py"&gt;ignoreregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;

&lt;span class="c"&gt;# /etc/fail2ban/jail.d/nextcloud.conf
&lt;/span&gt;&lt;span class="nn"&gt;[nextcloud]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;80,443&lt;/span&gt;
&lt;span class="py"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
&lt;span class="py"&gt;logpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/path/to/nextcloud/data/nextcloud.log&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3600&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Cloudflare Tunnels mean all traffic appears from Cloudflare IPs. Fail2ban won't be effective unless you configure the &lt;code&gt;TRUSTED_PROXIES&lt;/code&gt; setting and use the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header. For most homelab users, Nextcloud's built-in brute-force protection is sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Home Assistant Security
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/hass/configuration.yaml additions&lt;/span&gt;

&lt;span class="c1"&gt;# Require login&lt;/span&gt;
&lt;span class="na"&gt;homeassistant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;auth_providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;homeassistant&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;legacy_api_password&lt;/span&gt;  &lt;span class="c1"&gt;# Remove after migration&lt;/span&gt;

&lt;span class="c1"&gt;# IP ban after failed attempts&lt;/span&gt;
&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ip_ban_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;login_attempts_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;use_x_forwarded_for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;trusted_proxies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;172.16.0.0/12&lt;/span&gt;    &lt;span class="c1"&gt;# Docker networks&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;        &lt;span class="c1"&gt;# Localhost (cloudflared)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cloudflare Access (Zero Trust Authentication)
&lt;/h3&gt;

&lt;p&gt;For an extra layer, add Cloudflare Zero Trust authentication &lt;em&gt;before&lt;/em&gt; your service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create an Access application in Cloudflare Zero Trust dashboard&lt;/span&gt;
&lt;span class="c"&gt;# Settings → Zero Trust → Access → Applications → Add Application&lt;/span&gt;

&lt;span class="c"&gt;# Configuration:&lt;/span&gt;
&lt;span class="c"&gt;# Application name: Home Assistant&lt;/span&gt;
&lt;span class="c"&gt;# Domain: hass.commsnet.org&lt;/span&gt;
&lt;span class="c"&gt;# Policy: Email OTP (one-time password to your email)&lt;/span&gt;
&lt;span class="c"&gt;# Or: Identity provider (Google, GitHub, etc.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now visitors hit Cloudflare's login wall &lt;em&gt;before&lt;/em&gt; they even see Home Assistant. Two layers of authentication, zero ports open.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Monitoring and Backups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Health Checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add to your docker-compose.yml&lt;/span&gt;
  &lt;span class="na"&gt;watchtower&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;containrrr/watchtower&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;watchtower&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_CLEANUP=true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_SCHEDULE=0 0 4 * * *&lt;/span&gt;    &lt;span class="c1"&gt;# 4 AM daily&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATIONS=email&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_FROM=watchtower@commsnet.org&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_TO=admin@commsnet.org&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.example.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=${SMTP_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=${SMTP_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud-internal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backup Strategy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# backup-nextcloud.sh — Run daily via cron&lt;/span&gt;

&lt;span class="c"&gt;# 1. Put Nextcloud in maintenance mode&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ maintenance:mode &lt;span class="nt"&gt;--on&lt;/span&gt;

&lt;span class="c"&gt;# 2. Dump PostgreSQL&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;nextcloud-db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; nextcloud_db nextcloud &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /backups/nextcloud-db-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql

&lt;span class="c"&gt;# 3. Sync data directory&lt;/span&gt;
rsync &lt;span class="nt"&gt;-az&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; /path/to/nextcloud/data/ /backups/nextcloud-data/

&lt;span class="c"&gt;# 4. Take Nextcloud out of maintenance mode&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ maintenance:mode &lt;span class="nt"&gt;--off&lt;/span&gt;

&lt;span class="c"&gt;# 5. Upload to offsite (optional — Backblaze B2, S3, etc.)&lt;/span&gt;
&lt;span class="c"&gt;# rclone sync /backups/ remote:nextcloud-backups/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Home Assistant Backups
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Home Assistant automatic backups (in configuration.yaml)&lt;/span&gt;
&lt;span class="na"&gt;automation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Daily&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Backup"&lt;/span&gt;
    &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
        &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;03:00:00"&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backup.create&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Daily&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Backup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;now().strftime('%Y-%m-%d')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;keep_days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tunnel Won't Connect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check cloudflared logs&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; cloudflared &lt;span class="nt"&gt;-f&lt;/span&gt;

&lt;span class="c"&gt;# Common issues:&lt;/span&gt;
&lt;span class="c"&gt;# 1. DNS records not created — run `cloudflared tunnel route dns` again&lt;/span&gt;
&lt;span class="c"&gt;# 2. Credentials file missing — check ~/.cloudflared/ for the JSON file&lt;/span&gt;
&lt;span class="c"&gt;# 3. Firewall blocking outbound 7844 — Cloudflare uses this port for tunnel protocol&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Nextcloud Shows Wrong URL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Force the correct URL in config.php&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ config:system:set overwrite.cli.url &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://nextcloud.commsnet.org"&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ config:system:set overwriteprotocol &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Home Assistant Not Accessible
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check container health&lt;/span&gt;
docker inspect homeassistant | jq &lt;span class="s1"&gt;'.[0].State'&lt;/span&gt;

&lt;span class="c"&gt;# Check logs&lt;/span&gt;
docker logs homeassistant &lt;span class="nt"&gt;--tail&lt;/span&gt; 50

&lt;span class="c"&gt;# Common issue: WebSocket not upgrading&lt;/span&gt;
&lt;span class="c"&gt;# Ensure your Cloudflare tunnel config has noTLSVerify: true&lt;/span&gt;
&lt;span class="c"&gt;# and that you're using http:// not https:// in the service URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance: Nextcloud Slow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable Redis caching (verify in config.php)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ status

&lt;span class="c"&gt;# Check Redis connection&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;nextcloud-redis redis-cli &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REDIS_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ping

&lt;span class="c"&gt;# Run maintenance tasks&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ maintenance:repair
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data nextcloud-app php occ files:scan &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What You've Gained
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (ISP Defaults)&lt;/th&gt;
&lt;th&gt;After (This Setup)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CGNAT — no inbound connections&lt;/td&gt;
&lt;td&gt;Cloudflare Tunnel — outbound only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud storage subscriptions&lt;/td&gt;
&lt;td&gt;Self-hosted Nextcloud — your data, your rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposed ports for services&lt;/td&gt;
&lt;td&gt;Zero open inbound ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No home automation centralization&lt;/td&gt;
&lt;td&gt;Home Assistant with full control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic IP DDNS hacks&lt;/td&gt;
&lt;td&gt;Cloudflare-managed DNS with automatic TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No backup strategy&lt;/td&gt;
&lt;td&gt;Automated daily backups to offsite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single point of failure&lt;/td&gt;
&lt;td&gt;Health checks and automated recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Monthly Cost Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Cloud Subscription&lt;/th&gt;
&lt;th&gt;Self-Hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;File storage (1TB)&lt;/td&gt;
&lt;td&gt;Google One: $9.99/mo&lt;/td&gt;
&lt;td&gt;Nextcloud: $0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendar/Contacts&lt;/td&gt;
&lt;td&gt;Google: Free (data tax)&lt;/td&gt;
&lt;td&gt;Nextcloud: $0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Automation&lt;/td&gt;
&lt;td&gt;Nabu Casa: $6.50/mo&lt;/td&gt;
&lt;td&gt;HA Supervised: $0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Cloudflare: ~$10/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Tunnels&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Electricity (est.)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~$5-10/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$16.49/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$1/mo + electricity&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You break even in under a year, and you own your data forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get a domain&lt;/strong&gt; — transfer to Cloudflare or point nameservers there&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Cloudflare Tunnel&lt;/strong&gt; — 15 minutes, zero ports opened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Nextcloud&lt;/strong&gt; — Docker Compose up, configure, migrate your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Home Assistant&lt;/strong&gt; — start small, add integrations over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up backups&lt;/strong&gt; — automated, offsite, tested&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; — add Watchtower for updates, health checks for uptime&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Self-hosting isn't just about saving money. It's about owning your infrastructure, your data, and your time. When Google sunsets a product or Dropbox changes their terms, you won't care — because your files are on your hardware, behind your domain, on your terms.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CommsNet builds secure, sovereign infrastructure. More at &lt;a href="https://wiki.commsnet.org" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: #selfhosted #nextcloud #homeassistant #cloudflare #tunnels #docker #privacy #homelab #degoogling&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>networking</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Building a Secure Homelab with Proxmox VE, pfSense, and Cilium</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Mon, 22 Jun 2026 15:50:33 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/building-a-secure-homelab-with-proxmox-ve-pfsense-and-cilium-4e8m</link>
      <guid>https://dev.to/lunacommsnet/building-a-secure-homelab-with-proxmox-ve-pfsense-and-cilium-4e8m</guid>
      <description>&lt;h1&gt;
  
  
  Building a Secure Homelab with Proxmox VE, pfSense, and Cilium
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Published: May 25, 2026 | CommsNet&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through building a layered, observable homelab using three technologies that complement each other beautifully: &lt;strong&gt;Proxmox VE&lt;/strong&gt; for virtualization, &lt;strong&gt;pfSense&lt;/strong&gt; for network segmentation, and &lt;strong&gt;Cilium&lt;/strong&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Stack?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Proxmox VE — The Foundation
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;What matters for security:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate physical NICs&lt;/strong&gt; for WAN, LAN, and management — don't trunk everything over one interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux Bridge isolation&lt;/strong&gt; — each VLAN gets its own bridge, and bridges don't route between each other without explicit firewall rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AppArmor profiles&lt;/strong&gt; for LXC containers — even your unprivileged containers get kernel-level confinement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  pfSense — The Gatekeeper
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;What you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inter-VLAN firewalling&lt;/strong&gt; — your IoT VLAN cannot reach your server VLAN unless you write a rule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alias-based rules&lt;/strong&gt; — group IPs and ports by function, not by individual addresses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPN concentrator&lt;/strong&gt; — WireGuard or OpenVPN for remote access without exposing services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic shaping&lt;/strong&gt; — because your backup job shouldn't saturate your upload&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cilium — The Observer
&lt;/h3&gt;

&lt;p&gt;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 &lt;em&gt;and&lt;/em&gt; Layer 7.&lt;/p&gt;

&lt;p&gt;Why Cilium in a homelab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hubble&lt;/strong&gt; — real-time service map showing every connection between your workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network policies&lt;/strong&gt; — Kubernetes-native micro-segmentation that pfSense can't see inside your cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eBPF observability&lt;/strong&gt; — kernel-level tracing without modifying applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent encryption&lt;/strong&gt; — WireGuard-based encryption between nodes, managed declaratively&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────┐
                    │   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     ││
                               │  └─────────────┘│
                               └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Proxmox Network Foundation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Physical NIC Assignment
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check your NICs&lt;/span&gt;
ip &lt;span class="nb"&gt;link &lt;/span&gt;show

&lt;span class="c"&gt;# Assign roles in /etc/network/interfaces&lt;/span&gt;
&lt;span class="c"&gt;# eno1 → WAN (passed through to pfSense VM)&lt;/span&gt;
&lt;span class="c"&gt;# eno2 → vmbr0 (LAN - VLAN 10)&lt;/span&gt;
&lt;span class="c"&gt;# eno3 → vmbr1 (Server - VLAN 30)&lt;/span&gt;
&lt;span class="c"&gt;# eno4 → vmbr2 (IoT - VLAN 20)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bridge Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/network/interfaces on Proxmox host&lt;/span&gt;

&lt;span class="c"&gt;# Management bridge (access your Proxmox web UI)&lt;/span&gt;
auto vmbr0
iface vmbr0 inet static
    address 10.10.10.1/24
    bridge-ports eno2
    bridge-stp off
    bridge-fd 0

&lt;span class="c"&gt;# Server VLAN bridge&lt;/span&gt;
auto vmbr1
iface vmbr1 inet static
    address 10.30.10.1/24
    bridge-ports eno3
    bridge-stp off
    bridge-fd 0

&lt;span class="c"&gt;# IoT VLAN bridge (isolated)&lt;/span&gt;
auto vmbr2
iface vmbr2 inet static
    address 10.20.10.1/24
    bridge-ports eno4
    bridge-stp off
    bridge-fd 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Proxmox Firewall Basics
&lt;/h3&gt;

&lt;p&gt;Enable the datacenter firewall, but keep it simple initially. Block all by default, allow only what's needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/pve/firewall/cluster.fw&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;OPTIONS]
&lt;span class="nb"&gt;enable&lt;/span&gt;: 1
policy_in: DROP
policy_out: ACCEPT

&lt;span class="o"&gt;[&lt;/span&gt;RULES]
&lt;span class="c"&gt;# Allow SSH from management subnet only&lt;/span&gt;
IN ACCEPT &lt;span class="nt"&gt;-source&lt;/span&gt; 10.10.10.0/24 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;-dport&lt;/span&gt; 22 &lt;span class="nt"&gt;-log&lt;/span&gt; nolog
&lt;span class="c"&gt;# Allow Proxmox web UI from management subnet&lt;/span&gt;
IN ACCEPT &lt;span class="nt"&gt;-source&lt;/span&gt; 10.10.10.0/24 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;-dport&lt;/span&gt; 8006 &lt;span class="nt"&gt;-log&lt;/span&gt; nolog
&lt;span class="c"&gt;# Allow pfSense management&lt;/span&gt;
IN ACCEPT &lt;span class="nt"&gt;-source&lt;/span&gt; 10.10.10.0/24 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;-dport&lt;/span&gt; 443 &lt;span class="nt"&gt;-dest&lt;/span&gt; 10.10.10.2 &lt;span class="nt"&gt;-log&lt;/span&gt; nolog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: pfSense as VLAN Firewall
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VM Setup
&lt;/h3&gt;

&lt;p&gt;Create pfSense as a VM with at least 2 vCPUs and 2GB RAM. Attach NICs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Bridge&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vtnet0&lt;/td&gt;
&lt;td&gt;WAN passthrough&lt;/td&gt;
&lt;td&gt;Internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vtnet1&lt;/td&gt;
&lt;td&gt;vmbr0&lt;/td&gt;
&lt;td&gt;LAN (VLAN 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vtnet2&lt;/td&gt;
&lt;td&gt;vmbr1&lt;/td&gt;
&lt;td&gt;Server (VLAN 30)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vtnet3&lt;/td&gt;
&lt;td&gt;vmbr2&lt;/td&gt;
&lt;td&gt;IoT (VLAN 20)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Inter-VLAN Rules
&lt;/h3&gt;

&lt;p&gt;This is where most homelabs fall apart. If your IoT devices can reach your NAS, you've already lost. Here's the principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Default deny. Allow only what's explicitly needed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  LAN → Any (Trusted)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# LAN interface — your workstations and admin machines&lt;/span&gt;
&lt;span class="c"&gt;# Allow all outbound (your trusted users)&lt;/span&gt;
pass &lt;span class="k"&gt;in &lt;/span&gt;on vtnet1 from 10.10.10.0/24 to any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Server → LAN (Selective)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Server interface — K8s nodes, storage, services&lt;/span&gt;
&lt;span class="c"&gt;# Allow established connections back&lt;/span&gt;
pass &lt;span class="k"&gt;in &lt;/span&gt;on vtnet2 proto tcp from 10.30.10.0/24 to 10.10.10.0/24 port &lt;span class="o"&gt;{&lt;/span&gt; 22, 443, 6443 &lt;span class="o"&gt;}&lt;/span&gt; keep state
&lt;span class="c"&gt;# Block everything else&lt;/span&gt;
block &lt;span class="k"&gt;in &lt;/span&gt;on vtnet2 from 10.30.10.0/24 to 10.10.10.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  IoT → Only Internet (Locked Down)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# IoT interface — your smart devices, cameras, TVs&lt;/span&gt;
&lt;span class="c"&gt;# Allow DNS and NTP outbound&lt;/span&gt;
pass &lt;span class="k"&gt;in &lt;/span&gt;on vtnet3 proto &lt;span class="o"&gt;{&lt;/span&gt; tcp, udp &lt;span class="o"&gt;}&lt;/span&gt; from 10.20.10.0/24 to any port &lt;span class="o"&gt;{&lt;/span&gt; 53, 123 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# Allow HTTP/HTTPS outbound for firmware updates&lt;/span&gt;
pass &lt;span class="k"&gt;in &lt;/span&gt;on vtnet3 proto tcp from 10.20.10.0/24 to any port &lt;span class="o"&gt;{&lt;/span&gt; 80, 443 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# Block ALL access to internal networks&lt;/span&gt;
block &lt;span class="k"&gt;in &lt;/span&gt;on vtnet3 from 10.20.10.0/24 to &lt;span class="o"&gt;{&lt;/span&gt; 10.10.10.0/24, 10.30.10.0/24, 10.10.10.1, 10.30.10.1 &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aliases — Manage Rules at Scale
&lt;/h3&gt;

&lt;p&gt;Don't hardcode IPs. Use aliases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pfSense aliases (Diagnostics → Aliases)&lt;/span&gt;
TRUSTED_NET    &lt;span class="o"&gt;=&lt;/span&gt; 10.10.10.0/24
SERVER_NET     &lt;span class="o"&gt;=&lt;/span&gt; 10.30.10.0/24
IOT_NET        &lt;span class="o"&gt;=&lt;/span&gt; 10.20.10.0/24
DNS_PORTS      &lt;span class="o"&gt;=&lt;/span&gt; 53
NTP_PORTS      &lt;span class="o"&gt;=&lt;/span&gt; 123
ADMIN_PORTS    &lt;span class="o"&gt;=&lt;/span&gt; 22, 443, 8006
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your rules reference aliases, not IPs. When you add a new server subnet, update the alias, not every rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Cilium for Kubernetes Observability and Micro-Segmentation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;p&gt;Deploy Cilium as your CNI on your K8s cluster running inside Proxmox VMs or LXC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Cilium via Helm&lt;/span&gt;
helm repo add cilium https://helm.cilium.io/
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;cilium cilium/cilium &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;kubeProxyReplacement&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;strict &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; hubble.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; hubble.relay.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; hubble.ui.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; operator.replicas&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why kube-proxy Replacement Matters
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;kubeProxyReplacement=strict&lt;/code&gt; means Cilium replaces iptables-based kube-proxy entirely with eBPF. Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster service routing&lt;/strong&gt; — eBPF programs run in-kernel, no context switches to userspace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower latency&lt;/strong&gt; — direct packet processing at the socket layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent observability&lt;/strong&gt; — every connection goes through the same eBPF programs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No iptables drift&lt;/strong&gt; — one mechanism, not two&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hubble — See Every Connection
&lt;/h3&gt;

&lt;p&gt;Hubble is Cilium's observability layer. It streams every network connection in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Port-forward Hubble Relay&lt;/span&gt;
kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system svc/hubble-relay 4245:4245

&lt;span class="c"&gt;# Watch all connections in real time&lt;/span&gt;
hubble observe &lt;span class="nt"&gt;--since&lt;/span&gt; 1m

&lt;span class="c"&gt;# Filter by namespace&lt;/span&gt;
hubble observe &lt;span class="nt"&gt;--namespace&lt;/span&gt; production &lt;span class="nt"&gt;--since&lt;/span&gt; 5m

&lt;span class="c"&gt;# Track DNS resolution failures (classic IoT misbehavior indicator)&lt;/span&gt;
hubble observe &lt;span class="nt"&gt;--type&lt;/span&gt; trace:to-endpoint:dns &lt;span class="nt"&gt;--verdict&lt;/span&gt; DROPPED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Policies — Defense in Depth
&lt;/h3&gt;

&lt;p&gt;pfSense controls traffic &lt;em&gt;between&lt;/em&gt; VLANs. Cilium NetworkPolicies control traffic &lt;em&gt;inside&lt;/em&gt; your cluster. Both layers matter.&lt;/p&gt;

&lt;h4&gt;
  
  
  Default Deny All Ingress
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# default-deny.yaml — apply to every namespace&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default-deny-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Allow Specific Service Communication
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# nextcloud-policy.yaml — Nextcloud can reach its DB and Redis, nothing else&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;home-services&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Egress&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-ingress&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-nextcloud&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# Allow DNS&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;53&lt;/span&gt;
          &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UDP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  CiliumNetworkPolicy — L7 Visibility
&lt;/h4&gt;

&lt;p&gt;Standard Kubernetes NetworkPolicies are L3/L4. Cilium extends this to L7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cilium-l7-policy.yaml — restrict Nextcloud egress at HTTP level&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cilium.io/v2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CiliumNetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud-l7-egress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;home-services&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpointSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
  &lt;span class="na"&gt;egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;toFQDNs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matchName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updates.nextcloud.com"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matchName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;download.nextcloud.com"&lt;/span&gt;
      &lt;span class="na"&gt;toPorts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443"&lt;/span&gt;
          &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/.*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Observability Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Metrics Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cilium eBPF → Hubble → Prometheus → Grafana
     ↓
pfSense (netflow) → Prometheus → Grafana
     ↓
Proxmox (node metrics) → Prometheus → Grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Dashboards to Build
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inter-VLAN traffic&lt;/strong&gt; — who's talking to whom? Any IoT device hitting server subnets?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS queries per namespace&lt;/strong&gt; — spot DNS tunneling or C2 callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection drops per policy&lt;/strong&gt; — are your policies actually working?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pfSense rule hits&lt;/strong&gt; — which rules fire most? Tune your alias groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eBPF program latency&lt;/strong&gt; — should be sub-microsecond. If it spikes, you've got a problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Alerting Rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus alerts for your homelab security&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;homelab-security&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IoTDeviceReachingServerNet&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;hubble_flows_total{&lt;/span&gt;
            &lt;span class="s"&gt;source_namespace="iot",&lt;/span&gt;
            &lt;span class="s"&gt;destination_namespace="servers"&lt;/span&gt;
          &lt;span class="s"&gt;} &amp;gt; 0&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IoT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;device&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reaching&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;network"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UnexpectedDNSQueries&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;count(hubble_dns_queries_total) by (namespace, qname) &lt;/span&gt;
          &lt;span class="s"&gt;&amp;gt; 100&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unusual&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;DNS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;volume&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.namespace&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CiliumPolicyDrop&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cilium_policy_verdict_total{verdict="dropped"} &amp;gt; 0&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cilium&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;policy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dropping&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;traffic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expected&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;behavior"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Best Practices Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Proxmox
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable Proxmox firewall at datacenter level&lt;/li&gt;
&lt;li&gt;[ ] Use separate physical NICs for WAN, LAN, management&lt;/li&gt;
&lt;li&gt;[ ] Run LXC containers as unprivileged with AppArmor profiles&lt;/li&gt;
&lt;li&gt;[ ] Enable 2FA on Proxmox web UI&lt;/li&gt;
&lt;li&gt;[ ] Regular ZFS snapshots with automated send to offsite&lt;/li&gt;
&lt;li&gt;[ ] Keep Proxmox updated — subscribe to enterprise repo or use no-subscription&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  pfSense
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Default deny on all interfaces except LAN&lt;/li&gt;
&lt;li&gt;[ ] Use aliases, not hardcoded IPs, in rules&lt;/li&gt;
&lt;li&gt;[ ] Enable pfBlockerNG for ad/malware blocking at the gateway&lt;/li&gt;
&lt;li&gt;[ ] Set up WireGuard VPN for remote access — don't expose services&lt;/li&gt;
&lt;li&gt;[ ] Regular config backups (Automated via cron to offsite storage)&lt;/li&gt;
&lt;li&gt;[ ] Disable WAN responses — no ping, no admin interface on WAN&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cilium/Kubernetes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Default deny all ingress NetworkPolicy in every namespace&lt;/li&gt;
&lt;li&gt;[ ] Use CiliumNetworkPolicy for L7 egress restrictions&lt;/li&gt;
&lt;li&gt;[ ] Enable Hubble for real-time observability&lt;/li&gt;
&lt;li&gt;[ ] Run &lt;code&gt;kubeProxyReplacement=strict&lt;/code&gt; — don't mix iptables and eBPF&lt;/li&gt;
&lt;li&gt;[ ] Enable Cilium encryption (WireGuard) for inter-node traffic&lt;/li&gt;
&lt;li&gt;[ ] Audit NetworkPolicies regularly — use &lt;code&gt;kubectl get networkpolicies -A&lt;/code&gt; to review&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  General
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All management interfaces behind VPN or local access only&lt;/li&gt;
&lt;li&gt;[ ] SSH key-only authentication — disable password auth&lt;/li&gt;
&lt;li&gt;[ ] Automated security updates (unattended-upgrades on Debian/Ubuntu hosts)&lt;/li&gt;
&lt;li&gt;[ ] Centralized logging to a write-once destination&lt;/li&gt;
&lt;li&gt;[ ] Document your network topology — future you will thank present you&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What This Gets You
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compromised IoT device&lt;/td&gt;
&lt;td&gt;Can't reach server VLAN (pfSense drops it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lateral movement in K8s&lt;/td&gt;
&lt;td&gt;NetworkPolicy default deny + Cilium L7 rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data exfiltration from container&lt;/td&gt;
&lt;td&gt;Cilium FQDN egress policy + DNS monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unauthorized remote access&lt;/td&gt;
&lt;td&gt;WireGuard VPN, no exposed ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blind spots&lt;/td&gt;
&lt;td&gt;Hubble service map + Prometheus alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insider threat (rogue admin)&lt;/td&gt;
&lt;td&gt;Separate management VLAN + audit logging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Cost Breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Proxmox host&lt;/td&gt;
&lt;td&gt;Refurbished Dell OptiPlex (i5, 32GB RAM, 2TB NVMe)&lt;/td&gt;
&lt;td&gt;~$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NICs&lt;/td&gt;
&lt;td&gt;Intel X710-T2L (2.5GbE, SR-IOV capable)&lt;/td&gt;
&lt;td&gt;~$80 each × 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pfSense&lt;/td&gt;
&lt;td&gt;VM on Proxmox (free CE edition)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cilium&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch&lt;/td&gt;
&lt;td&gt;Used managed switch (VLAN-capable)&lt;/td&gt;
&lt;td&gt;~$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$400&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's enterprise-grade network segmentation for less than a single rack-mount firewall license.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with Proxmox&lt;/strong&gt; — get your VMs running, assign NICs properly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add pfSense&lt;/strong&gt; — configure VLANs before you deploy services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy K8s with Cilium&lt;/strong&gt; — enable Hubble from day one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer in policies&lt;/strong&gt; — default deny first, then add specific allows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt; — watch Hubble for a week before you think you know your traffic patterns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CommsNet builds secure, observable infrastructure. More at &lt;a href="https://wiki.commsnet.org" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: #homelab #proxmox #pfsense #cilium #ebpf #kubernetes #networking #security #selfhosted&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosting</category>
      <category>security</category>
      <category>infrastructure</category>
    </item>
  </channel>
</rss>
