<?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>Building Reusable Agent Skills for OpenClaw</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Sat, 22 Aug 2026 13:31:31 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/building-reusable-agent-skills-for-openclaw-5cg4</link>
      <guid>https://dev.to/lunacommsnet/building-reusable-agent-skills-for-openclaw-5cg4</guid>
      <description>&lt;h1&gt;
  
  
  Building Reusable Agent Skills for OpenClaw
&lt;/h1&gt;




&lt;h2&gt;
  
  
  1. What Are OpenClaw Skills?
&lt;/h2&gt;

&lt;p&gt;OpenClaw skills are reusable, packaged procedures that give AI agents specialized capabilities. Think of them as plugins — but instead of code that runs blindly, they're instructional documents that guide an agent through a specific workflow.&lt;/p&gt;

&lt;p&gt;A skill can be anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A security audit checklist&lt;/li&gt;
&lt;li&gt;A deployment procedure for a specific stack&lt;/li&gt;
&lt;li&gt;A research methodology&lt;/li&gt;
&lt;li&gt;A content publishing workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent reads the skill, understands the steps, and executes them using its existing tools (shell, browser, API calls, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Why Skills Matter
&lt;/h2&gt;

&lt;p&gt;Without skills, every agent starts from scratch on every task. With skills:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — Same procedure every time, across agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shareability&lt;/strong&gt; — Publish to ClawHub, other agents can install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning&lt;/strong&gt; — Skills have versions, can be updated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability&lt;/strong&gt; — Agents can chain multiple skills&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Skill Structure
&lt;/h2&gt;

&lt;p&gt;A minimal skill needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-skill/
├── SKILL.md          # Main procedure document
├── examples/         # Optional: example files
├── scripts/          # Optional: helper scripts
└── references/       # Optional: reference docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SKILL.md Format
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# My Skill Name&lt;/span&gt;

&lt;span class="gu"&gt;## When to Use&lt;/span&gt;
Describe when this skill should be triggered.

&lt;span class="gu"&gt;## Prerequisites&lt;/span&gt;
List any required tools, access, or setup.

&lt;span class="gu"&gt;## Procedure&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Step one
&lt;span class="p"&gt;2.&lt;/span&gt; Step two
&lt;span class="p"&gt;3.&lt;/span&gt; Step three

&lt;span class="gu"&gt;## Verification&lt;/span&gt;
How to verify the task was completed correctly.

&lt;span class="gu"&gt;## Troubleshooting&lt;/span&gt;
Common issues and fixes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Real Example: pfSense Security Audit Skill
&lt;/h2&gt;

&lt;p&gt;I built a pfSense diagnosis skill for my homelab. Here's the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pfsense-diagnosis/
├── SKILL.md
├── references/
│   ├── hardening-checklist.md
│   └── common-issues.md
└── scripts/
    └── firewall-audit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SKILL.md walks through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSH to pfSense&lt;/li&gt;
&lt;li&gt;Check firmware version&lt;/li&gt;
&lt;li&gt;Review firewall rules&lt;/li&gt;
&lt;li&gt;Audit open ports&lt;/li&gt;
&lt;li&gt;Check CARP status&lt;/li&gt;
&lt;li&gt;Review VPN configuration&lt;/li&gt;
&lt;li&gt;Generate report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any agent with SSH access can now run a pfSense security audit without me explaining the steps each time.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Publishing to ClawHub
&lt;/h2&gt;

&lt;p&gt;ClawHub is the skill marketplace. Publishing makes your skill available to other OpenClaw users.&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;# Publish&lt;/span&gt;
openclaw skills publish my-skill/

&lt;span class="c"&gt;# Install&lt;/span&gt;
openclaw skills &lt;span class="nb"&gt;install&lt;/span&gt; @username/my-skill

&lt;span class="c"&gt;# List installed&lt;/span&gt;
openclaw skills list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Skill Ideas for Homelab Operators
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Demand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UniFi Security Audit&lt;/td&gt;
&lt;td&gt;Audit UniFi controller config&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxmox Backup Verification&lt;/td&gt;
&lt;td&gt;Test backup restore&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nextcloud Health Check&lt;/td&gt;
&lt;td&gt;Diagnose Nextcloud issues&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zabbix Alert Triage&lt;/td&gt;
&lt;td&gt;Classify and prioritize alerts&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Assistant Backup&lt;/td&gt;
&lt;td&gt;Export and verify HA config&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose Validator&lt;/td&gt;
&lt;td&gt;Check configs for security issues&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL Certificate Monitor&lt;/td&gt;
&lt;td&gt;Track expiry dates&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  7. Building Quality Skills
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Write clear, step-by-step procedures&lt;/li&gt;
&lt;li&gt;Include verification steps&lt;/li&gt;
&lt;li&gt;Add troubleshooting for common failures&lt;/li&gt;
&lt;li&gt;Test with agents that didn't write the skill&lt;/li&gt;
&lt;li&gt;Version your skills when updating&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Don't
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Write vague instructions ("check the firewall")&lt;/li&gt;
&lt;li&gt;Assume tools are installed (check first)&lt;/li&gt;
&lt;li&gt;Hardcode credentials or IPs&lt;/li&gt;
&lt;li&gt;Skip verification steps&lt;/li&gt;
&lt;li&gt;Make skills dependent on specific agent personalities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. The Skill Workshop
&lt;/h2&gt;

&lt;p&gt;OpenClaw includes a Skill Workshop for creating and reviewing skills:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Draft&lt;/strong&gt; — Write the skill procedure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; — The workshop validates structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; — Skill goes live for your agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; — Optional: share to ClawHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures skills meet quality standards before deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Composability: Chaining Skills
&lt;/h2&gt;

&lt;p&gt;Skills can reference other skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Procedure&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run the &lt;span class="sb"&gt;`pfsense-diagnosis`&lt;/span&gt; skill to check firewall health
&lt;span class="p"&gt;2.&lt;/span&gt; Run the &lt;span class="sb"&gt;`docker-compose-validator`&lt;/span&gt; skill on all compose files
&lt;span class="p"&gt;3.&lt;/span&gt; Cross-reference findings and generate combined report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you build complex workflows from simple, reusable components.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw docs:&lt;/strong&gt; &lt;a href="https://docs.openclaw.ai" rel="noopener noreferrer"&gt;https://docs.openclaw.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClawHub:&lt;/strong&gt; Search &lt;code&gt;openclaw skills search&lt;/code&gt; in your terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;https://github.com/openclaw/openclaw&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Skills are how agents learn. Share yours.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>openclaw</category>
      <category>skills</category>
    </item>
    <item>
      <title>Building a Gitea-First Code Review Tool in Go</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Sat, 22 Aug 2026 11:59:11 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/building-a-gitea-first-code-review-tool-in-go-55mc</link>
      <guid>https://dev.to/lunacommsnet/building-a-gitea-first-code-review-tool-in-go-55mc</guid>
      <description>&lt;h1&gt;
  
  
  Building a Gitea-First Code Review Tool in Go
&lt;/h1&gt;




&lt;h2&gt;
  
  
  1. The Problem: Gitea Has No Code Review Automation
&lt;/h2&gt;

&lt;p&gt;If you self-host Gitea, you already know why: it's lightweight, fast, and doesn't require a GitHub enterprise budget. But there's a gap. GitHub has CodeRabbit, Graphite, and GitHub's own Copilot code review. GitLab has its own analysis. Gitea? Nothing.&lt;/p&gt;

&lt;p&gt;You're left with two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manual review&lt;/strong&gt; — read every diff yourself, every time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI-only checks&lt;/strong&gt; — linting and tests run in Actions/Act Runner, but nobody looks at the &lt;em&gt;semantic&lt;/em&gt; content of changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither catches what a good code review catches: security vulnerabilities introduced in a PR, hardcoded secrets, deprecated patterns, or logic bugs that pass linting.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;strong&gt;Repository Detective&lt;/strong&gt; — a Gitea-first code review tool that combines deterministic scanners with optional LLM analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architecture: Deterministic First, LLM Second
&lt;/h2&gt;

&lt;p&gt;The key design decision: &lt;strong&gt;deterministic scanners run first, LLM analysis runs second on flagged files only.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't "AI reviews your code." It's "scanners find candidates, then LLM validates and explains them with proof-of-concept."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Order Matters
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LLM-only&lt;/td&gt;
&lt;td&gt;Hallucinations, no verifiable evidence, vibes-based findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanner-only&lt;/td&gt;
&lt;td&gt;No context, false positives, developer ignores noisy alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scanner → LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scanners find real candidates, LLM validates and writes PoC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Pipeline Stages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push/PR → Webhook → Diff Extraction
                           ↓
                    Stage 1: Deterministic Scanners
                    (Trivy, Grype, golangci-lint, ruff, shellcheck)
                           ↓
                    Stage 2: LLM Analysis (optional, off by default)
                    (OpenAI-compatible endpoint, only on flagged files)
                           ↓
                    Stage 3: Issue Creation
                    (Gitea issues with severity, file:line, code snippet, PoC)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Tech Stack
&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;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Single binary, fast, Gitea is Go too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;Native Gitea webhook&lt;/td&gt;
&lt;td&gt;Push and PR events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanners&lt;/td&gt;
&lt;td&gt;Trivy, Grype, golangci-lint, ruff, shellcheck&lt;/td&gt;
&lt;td&gt;Best-in-class, open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Any OpenAI-compatible endpoint&lt;/td&gt;
&lt;td&gt;Privacy: runs on your infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;Docker + docker-compose&lt;/td&gt;
&lt;td&gt;Easy self-hosted deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;Community open source, commercial tiers available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. Quick Start
&lt;/h2&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.minimal.yml — included in repo&lt;/span&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;repository-detective&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;ghcr.io/commstech/repository-detective:latest&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;8080:8080"&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;GITEA_URL=https://your-gitea-instance.com&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITEA_TOKEN=your-webhook-secret&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBHOOK_SECRET=your-webhook-secret&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:/app/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Start the container&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# 2. Add webhook in Gitea repo settings&lt;/span&gt;
&lt;span class="c"&gt;#    URL: http://your-server:8080/webhook&lt;/span&gt;
&lt;span class="c"&gt;#    Content type: application/json&lt;/span&gt;
&lt;span class="c"&gt;#    Secret: your-webhook-secret&lt;/span&gt;
&lt;span class="c"&gt;#    Trigger: Push events, Pull request events&lt;/span&gt;

&lt;span class="c"&gt;# 3. Push code. Issues appear automatically.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. What It Catches
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example Finding (created as Gitea issue)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;🔍&lt;/span&gt; &lt;span class="n"&gt;Repository&lt;/span&gt; &lt;span class="n"&gt;Detective&lt;/span&gt; &lt;span class="n"&gt;Finding&lt;/span&gt;
&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HIGH&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;
&lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;47&lt;/span&gt;

&lt;span class="n"&gt;Issue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;compared&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;instead&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;
       &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;allows&lt;/span&gt; &lt;span class="n"&gt;timing&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="n"&gt;attacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Evidence&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expectedToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="n"&gt;Proof&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;Concept&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;An&lt;/span&gt; &lt;span class="n"&gt;attacker&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="n"&gt;measure&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="n"&gt;differences&lt;/span&gt;
  &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;brute&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;force&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Recommended&lt;/span&gt; &lt;span class="n"&gt;Fix&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                 &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedToken&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;p&gt;This is the kind of finding that scanners alone miss (it's not a known CVE pattern) and LLM-only tools hallucinate (without scanner context). The combination catches real issues with verifiable evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Privacy-First Design
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM is off by default.&lt;/strong&gt; You can run scanner-only mode with zero external API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When LLM is enabled&lt;/strong&gt;, it only sends flagged file diffs (not your entire codebase) to the endpoint you configure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted LLM supported.&lt;/strong&gt; Point it at your local Ollama, vLLM, or any OpenAI-compatible server. Your code never leaves your infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No telemetry, no phone-home, no analytics.&lt;/strong&gt; Repository Detective doesn't report back anywhere.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Open Core Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free (AGPL-3.0)&lt;/td&gt;
&lt;td&gt;All scanners, webhook integration, Gitea issues, single repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$100/instance/month&lt;/td&gt;
&lt;td&gt;Multi-repo, custom scanner configs, Slack/Discord notifications, priority fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contact us&lt;/td&gt;
&lt;td&gt;SSO/OIDC, audit trail, on-prem LLM bundle, custom integrations, SLA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The community tier is fully functional. No paywalled security features. The paid tiers add convenience and scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] Gitea webhook integration (push + PR)&lt;/li&gt;
&lt;li&gt;[x] Trivy, Grype, golangci-lint, ruff, shellcheck&lt;/li&gt;
&lt;li&gt;[x] Optional LLM analysis with PoC generation&lt;/li&gt;
&lt;li&gt;[x] Gitea issue creation with severity tagging&lt;/li&gt;
&lt;li&gt;[x] Docker deployment&lt;/li&gt;
&lt;li&gt;[ ] GitHub support (next priority)&lt;/li&gt;
&lt;li&gt;[ ] Slack/Discord/Telegram notifications (Pro tier)&lt;/li&gt;
&lt;li&gt;[ ] Multi-repo dashboard (Pro tier)&lt;/li&gt;
&lt;li&gt;[ ] SSO/OIDC (Enterprise tier)&lt;/li&gt;
&lt;li&gt;[ ] SBOM generation and drift detection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9. Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/CommsTech/Repository-Detective" rel="noopener noreferrer"&gt;https://github.com/CommsTech/Repository-Detective&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gitea (build repo):&lt;/strong&gt; &lt;a href="https://git.commsnet.org/commstech/Repository-Detective" rel="noopener noreferrer"&gt;https://git.commsnet.org/commstech/Repository-Detective&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; AGPL-3.0-or-later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker:&lt;/strong&gt; &lt;code&gt;ghcr.io/commstech/repository-detective:latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Feedback Wanted
&lt;/h2&gt;

&lt;p&gt;I'm building this because Gitea users deserve code review tooling too. If you self-host Gitea, I want to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What scanners would you add to the pipeline?&lt;/li&gt;
&lt;li&gt;Does the LLM-optional approach work for your security posture?&lt;/li&gt;
&lt;li&gt;What's missing for you to deploy this today?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Open issues on GitHub or start a discussion. I'm actively developing based on user feedback.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Repository Detective is AGPL-3.0 open source. Self-host your code review.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gitea</category>
      <category>codequality</category>
      <category>go</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Automated Homelab Backups: Borg and Restic for Disaster Recovery</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Sat, 22 Aug 2026 11:56:53 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/automated-homelab-backups-borg-and-restic-for-disaster-recovery-466p</link>
      <guid>https://dev.to/lunacommsnet/automated-homelab-backups-borg-and-restic-for-disaster-recovery-466p</guid>
      <description>&lt;h1&gt;
  
  
  Automated Homelab Backups: Borg and Restic for Disaster Recovery
&lt;/h1&gt;




&lt;h2&gt;
  
  
  1. Introduction — Why Automated Backups Matter for Homelabs
&lt;/h2&gt;

&lt;p&gt;Your homelab is running 15 Docker containers, three VMs, a Nextcloud instance with family photos, and a Gitea server with two years of code commits. When was the last time you tested a restore?&lt;/p&gt;

&lt;p&gt;If you're like most homelab operators, the answer is "never" or "I think it worked when I set it up." That's not a backup — that's a prayer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes a Good Backup Strategy?
&lt;/h3&gt;

&lt;p&gt;The 3-2-1 rule, established by photographer Peter Krogh, remains the gold standard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3&lt;/strong&gt; copies of your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; different storage media (e.g., local NAS + offsite VPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; copy offsite (cloud, VPS, or friend's server)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good backup system is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated&lt;/strong&gt; — if it requires manual steps, you'll skip it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitored&lt;/strong&gt; — silent failures are worse than no backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested&lt;/strong&gt; — an untested backup is a hypothesis, not a guarantee&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned&lt;/strong&gt; — you need to restore from last Tuesday, not just "the latest"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Borg vs Restic: Which One?
&lt;/h3&gt;

&lt;p&gt;Both are excellent, modern, deduplicating backup tools. Here's how to choose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Borg&lt;/th&gt;
&lt;th&gt;Restic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python (C extensions)&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repository format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deduplication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chunk-level, excellent&lt;/td&gt;
&lt;td&gt;Chunk-level, excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;lz4, zlib, zstd&lt;/td&gt;
&lt;td&gt;(none built-in, use external)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Encryption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optional (repo-level)&lt;/td&gt;
&lt;td&gt;Mandatory (always encrypted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backends&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local, SSH/SFTP&lt;/td&gt;
&lt;td&gt;Local, SFTP, S3, Backblaze, REST server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Borgmatic, BorgWarehouse&lt;/td&gt;
&lt;td&gt;Restic Profile, Restic Browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mount backup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (FUSE)&lt;/td&gt;
&lt;td&gt;Yes (FUSE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2010, very mature&lt;/td&gt;
&lt;td&gt;2014, mature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local + SSH backups&lt;/td&gt;
&lt;td&gt;Cloud/S3 backups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Config format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YAML (via borgmatic)&lt;/td&gt;
&lt;td&gt;TOML (via resticprofile)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;My recommendation for homelabs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Borg&lt;/strong&gt; for local/NAS backups (better compression, mature tooling)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Restic&lt;/strong&gt; for offsite/S3 backups (native cloud backend support)&lt;/li&gt;
&lt;li&gt;Run both in parallel for defense in depth&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Prerequisites
&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;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Proxmox VE host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The thing you're backing up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backup server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local NAS, dedicated backup machine, or VPS ($3-5/mo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSH access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Between Proxmox host and backup server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Root/sudo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On both Proxmox and backup server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage space&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;At least 2x your data size (dedup helps, but first backup is full)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Storage Planning
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data Type&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Dedup Ratio&lt;/th&gt;
&lt;th&gt;Backup Space Needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VM disk images&lt;/td&gt;
&lt;td&gt;50-200GB each&lt;/td&gt;
&lt;td&gt;~1.5x (sparse files)&lt;/td&gt;
&lt;td&gt;100-300GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nextcloud files&lt;/td&gt;
&lt;td&gt;10-100GB&lt;/td&gt;
&lt;td&gt;~1.2x (already compressed)&lt;/td&gt;
&lt;td&gt;20-120GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker volumes&lt;/td&gt;
&lt;td&gt;5-50GB&lt;/td&gt;
&lt;td&gt;~2x (good dedup)&lt;/td&gt;
&lt;td&gt;5-25GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Assistant config&lt;/td&gt;
&lt;td&gt;&amp;lt;1GB&lt;/td&gt;
&lt;td&gt;~3x (tiny, dedup-friendly)&lt;/td&gt;
&lt;td&gt;&amp;lt;1GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gitea repos&lt;/td&gt;
&lt;td&gt;1-10GB&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;td&gt;1-5GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Borg's deduplication is excellent across similar VMs. If you back up 5 Ubuntu VMs that share the same base packages, dedup can reduce total backup size by 60-80%.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Borg Backup Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Installing Borg
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On your Proxmox host (the source):&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="nb"&gt;sudo &lt;/span&gt;apt update
&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; borgbackup borgmatic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On your backup server (the target):&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="nb"&gt;sudo &lt;/span&gt;apt update
&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; borgbackup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Borg 1.2+ is recommended. Check your version with &lt;code&gt;borg --version&lt;/code&gt;. If your distro ships an older version, install from the official Borg repo or use the static binary from &lt;a href="https://github.com/borgbackup/borg/releases" rel="noopener noreferrer"&gt;https://github.com/borgbackup/borg/releases&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.2 Initialize a Borg Repository
&lt;/h3&gt;

&lt;p&gt;On your Proxmox host, create a repository on the backup 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;# Create a directory on the backup server (via SSH)&lt;/span&gt;
ssh backupuser@backup-server &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /backups/proxmox

&lt;span class="c"&gt;# Initialize the Borg repository with encryption&lt;/span&gt;
&lt;span class="c"&gt;# You'll be prompted to set a repository password — SAVE THIS SECURELY&lt;/span&gt;
borg init &lt;span class="nt"&gt;--encryption&lt;/span&gt; repokey-blake2 backupuser@backup-server:/backups/proxmox

&lt;span class="c"&gt;# Verify the repository&lt;/span&gt;
borg info backupuser@backup-server:/backups/proxmox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Warning:&lt;/strong&gt; If you lose the repository password, your backups are unrecoverable. Store it in a password manager (Bitwarden, KeePassXC) and keep an offline copy in a safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.3 Creating Your First 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;# Create a backup of /etc and /var/lib/docker&lt;/span&gt;
borg create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stats&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--progress&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compression&lt;/span&gt; zstd,3 &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/proxmox::&lt;span class="s2"&gt;"{hostname}-{now}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc &lt;span class="se"&gt;\&lt;/span&gt;
  /var/lib/docker &lt;span class="se"&gt;\&lt;/span&gt;
  /var/lib/libvirt &lt;span class="se"&gt;\&lt;/span&gt;
  /root

&lt;span class="c"&gt;# The archive name uses {hostname}-{now} which expands to:&lt;/span&gt;
&lt;span class="c"&gt;# proxmox-2026-07-26T01:46:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Understanding the command:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--stats&lt;/code&gt; — show dedup/compression statistics after completion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--progress&lt;/code&gt; — show a progress bar during backup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--compression zstd,3&lt;/code&gt; — use Zstandard compression level 3 (good balance of speed/ratio)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;::"{hostname}-{now}"&lt;/code&gt; — archive name template (hostname + timestamp)&lt;/li&gt;
&lt;li&gt;The paths at the end are what gets backed up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output example:&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;Archive name: proxmox-2026-07-26T01:46:00
Archive fingerprint: a1b2c3d4e5...
Time (start): Sat Jul 26 01:46:00 2026
Time (end):   Sat Jul 26 01:48:32 2026
Duration: 2 minutes 32 seconds
Number of files: 45283
Original size: 12.5 GB
Deduplicated size: 4.2 GB (66% reduction)
Compressed size: 3.1 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.4 Borgmatic Configuration for Automation
&lt;/h3&gt;

&lt;p&gt;Borgmatic is a YAML-based wrapper around Borg that makes automation much cleaner:&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;# /etc/borgmatic.d/proxmox.yaml&lt;/span&gt;
&lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backupuser@backup-server:/backups/proxmox&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nas&lt;/span&gt;
  &lt;span class="na"&gt;source_directories&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&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/docker&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/libvirt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/root&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/home&lt;/span&gt;
  &lt;span class="na"&gt;exclude_patterns&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/lib/docker/overlay2/*/diff/tmp/*&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/libvirt/images/*.iso&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;**&lt;/span&gt;&lt;span class="s"&gt;/__pycache__&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;**&lt;/span&gt;&lt;span class="s"&gt;/node_modules&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="err"&gt;**&lt;/span&gt;&lt;span class="s"&gt;/.cache&lt;/span&gt;

&lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zstd,3&lt;/span&gt;
  &lt;span class="na"&gt;encryption_passcommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cat&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/etc/borgmatic.d/passphrase"&lt;/span&gt;
  &lt;span class="na"&gt;ssh_command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh -i /root/.ssh/backup_key&lt;/span&gt;

&lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;keep_daily&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
  &lt;span class="na"&gt;keep_weekly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;keep_monthly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
  &lt;span class="na"&gt;keep_yearly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;before_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Starting backup at $(date)"&lt;/span&gt;
  &lt;span class="na"&gt;after_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Backup completed at $(date)"&lt;/span&gt;
  &lt;span class="na"&gt;on_error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -s -X POST https://ntfy.commsnet.org/backup-error \&lt;/span&gt;
      &lt;span class="s"&gt;-d "Borg backup FAILED on $(hostname) at $(date)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create the passphrase file:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"your-super-secure-passphrase"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/borgmatic.d/passphrase
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/borgmatic.d/passphrase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test the config:&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="nb"&gt;sudo &lt;/span&gt;borgmatic create &lt;span class="nt"&gt;--verbosity&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Restic Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Installing Restic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;On your Proxmox host:&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;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&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; restic

&lt;span class="c"&gt;# Or install the latest binary directly&lt;/span&gt;
wget https://github.com/restic/restic/releases/download/v0.16.4/restic_0.16.4_linux_amd64.bz2
bunzip2 restic_0.16.4_linux_amd64.bz2
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;restic_0.16.4_linux_amd64 /usr/local/bin/restic
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/restic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 Configuring a Repository
&lt;/h3&gt;

&lt;p&gt;Restic supports multiple backends. Here are the most useful for homelabs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local or SFTP (SSH):&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;# Local directory (e.g., mounted NAS)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/mnt/nas/backups/proxmox-restic"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/restic/passphrase"&lt;/span&gt;

&lt;span class="c"&gt;# Or SFTP (remote server via SSH)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sftp:backupuser@backup-server:/backups/proxmox-restic"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/restic/passphrase"&lt;/span&gt;

restic init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;S3-compatible (MinIO, Backblaze B2, Cloudflare R2):&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"s3:https://s3.backblazeb2.com/commsnet-backups"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RESTIC_PASSWORD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/restic/passphrase"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-key-id"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-secret-key"&lt;/span&gt;

restic init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Backblaze B2 offers 10GB free storage — enough for small homelab configs and documents. For larger backups, their pricing ($0.005/GB/month) is very competitive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.3 Creating Snapshots
&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 a snapshot&lt;/span&gt;
restic backup /etc /var/lib/docker /var/lib/libvirt /root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag&lt;/span&gt; proxmox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag&lt;/span&gt; daily &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--verbose&lt;/span&gt;

&lt;span class="c"&gt;# List snapshots&lt;/span&gt;
restic snapshots

&lt;span class="c"&gt;# Show snapshot details&lt;/span&gt;
restic stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.4 Restic Profiles for Automation
&lt;/h3&gt;

&lt;p&gt;For cleaner automation, use &lt;code&gt;resticprofile&lt;/code&gt; — a config-driven wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/resticprofile/proxmox.conf&lt;/span&gt;
&lt;span class="nn"&gt;[default]&lt;/span&gt;
&lt;span class="py"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sftp:backupuser@backup-server:/backups/proxmox-restic"&lt;/span&gt;
&lt;span class="py"&gt;password_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/restic/passphrase"&lt;/span&gt;

&lt;span class="nn"&gt;[default.backup]&lt;/span&gt;
&lt;span class="py"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/etc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/libvirt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/root"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;exclude&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/var/lib/docker/overlay2/*/diff/tmp/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"**/__pycache__"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"**/node_modules"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"proxmox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"daily"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;schedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"daily"&lt;/span&gt;
&lt;span class="py"&gt;schedule_permision&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"system"&lt;/span&gt;

&lt;span class="nn"&gt;[default.retention]&lt;/span&gt;
&lt;span class="py"&gt;keep_daily&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="py"&gt;keep_weekly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;keep_monthly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="py"&gt;keep_yearly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="nn"&gt;[default.check]&lt;/span&gt;
&lt;span class="py"&gt;schedule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"weekly"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install resticprofile and enable:&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 resticprofile&lt;/span&gt;
wget https://github.com/creativeprojects/resticprofile/releases/download/v0.17.0/resticprofile_0.17.0_linux_amd64.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf resticprofile_0.17.0_linux_amd64.tar.gz
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;resticprofile /usr/local/bin/

&lt;span class="c"&gt;# Schedule with systemd&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;resticprofile schedule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Backing Up Proxmox VMs
&lt;/h2&gt;

&lt;p&gt;Proxmox has its own backup system (&lt;code&gt;vzdump&lt;/code&gt;), but it stores backups locally by default. Here's how to combine it with Borg/Restic for offsite copies:&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Configure vzdump for Local Backups
&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/vzdump.conf&lt;/span&gt;
dumpdir: /var/lib/vz/dump
storage: &lt;span class="nb"&gt;local
&lt;/span&gt;compress: zstd
mode: snapshot  &lt;span class="c"&gt;# Use snapshot mode for running VMs (requires LVM/thin)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Schedule VM Backups in Proxmox GUI
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Datacenter → Backup → Add&lt;/li&gt;
&lt;li&gt;Set:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; local-btrfs (or your backup storage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule:&lt;/strong&gt; 02:00 daily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selection:&lt;/strong&gt; All VMs (or specific IDs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode:&lt;/strong&gt; Snapshot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression:&lt;/strong&gt; Zstd&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  5.3 Offsite the vzdump Backups with Borg
&lt;/h3&gt;

&lt;p&gt;Add the vzdump backup directory to your borgmatic config:&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;# Add to /etc/borgmatic.d/proxmox.yaml source_directories:&lt;/span&gt;
&lt;span class="na"&gt;source_directories&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&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/docker&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/libvirt&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/vz/dump&lt;/span&gt;  &lt;span class="c1"&gt;# Proxmox VM backups&lt;/span&gt;

&lt;span class="c1"&gt;# Exclude large ISO images&lt;/span&gt;
&lt;span class="na"&gt;exclude_patterns&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/lib/vz/dump/*.iso&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/vz/template/iso/*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Alternative: Hook Borg into vzdump
&lt;/h3&gt;

&lt;p&gt;Create a hook script that runs Borg after vzdump completes:&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;# /usr/local/bin/vzdump-hook-borg.sh&lt;/span&gt;
&lt;span class="c"&gt;# Called by vzdump with $1=phase, $2=vmid&lt;/span&gt;

&lt;span class="nv"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;vmid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$phase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"job-end"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# Run Borg backup of the vzdump directory after all VMs are dumped&lt;/span&gt;
    /usr/bin/borgmatic create &lt;span class="nt"&gt;--stats&lt;/span&gt; &lt;span class="nt"&gt;--progress&lt;/span&gt; 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; borgmatic

    &lt;span class="c"&gt;# Also run restic for offsite&lt;/span&gt;
    /usr/bin/restic backup /var/lib/vz/dump &lt;span class="nt"&gt;--tag&lt;/span&gt; proxmox-vm &lt;span class="nt"&gt;--tag&lt;/span&gt; vzdump 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; restic
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make it executable&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/vzdump-hook-borg.sh

&lt;span class="c"&gt;# Add to vzdump job (via Proxmox GUI or CLI):&lt;/span&gt;
&lt;span class="c"&gt;# vzdump --hookscript /usr/local/bin/vzdump-hook-borg.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Backing Up Critical Services
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Nextcloud Data
&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;# Nextcloud maintenance mode (ensures consistent backup)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data php /var/www/nextcloud/occ maintenance:mode &lt;span class="nt"&gt;--on&lt;/span&gt;

&lt;span class="c"&gt;# Borg backup of Nextcloud data and config&lt;/span&gt;
borg create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compression&lt;/span&gt; zstd,3 &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/nextcloud::&lt;span class="s2"&gt;"{now}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/www/nextcloud/config &lt;span class="se"&gt;\&lt;/span&gt;
  /var/www/nextcloud/data &lt;span class="se"&gt;\&lt;/span&gt;
  /var/www/nextcloud/apps

&lt;span class="c"&gt;# Turn off maintenance mode&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; www-data php /var/www/nextcloud/occ maintenance:mode &lt;span class="nt"&gt;--off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to borgmatic with pre/post hooks:&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;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;before_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --on&lt;/span&gt;
  &lt;span class="na"&gt;after_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --off&lt;/span&gt;
  &lt;span class="na"&gt;on_error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sudo -u www-data php /var/www/nextcloud/occ maintenance:mode --off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.2 Home Assistant 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;# Home Assistant has a built-in backup mechanism&lt;/span&gt;
&lt;span class="c"&gt;# Trigger it via API before backing up&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&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;HA_TOKEN&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="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://homeassistant:8123/api/services/backup/generate

&lt;span class="c"&gt;# Then backup the snapshots directory&lt;/span&gt;
borg create &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/ha::&lt;span class="s2"&gt;"{now}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /config/backups &lt;span class="se"&gt;\&lt;/span&gt;
  /config/configuration.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  /config/automations.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 Docker Volumes
&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;# Stop containers for consistent backup, or use --ignore-fs-type&lt;/span&gt;
&lt;span class="c"&gt;# Option A: Stop, backup, start (safest)&lt;/span&gt;
docker compose stop
borg create backupuser@backup-server:/backups/docker::&lt;span class="s2"&gt;"{now}"&lt;/span&gt; /var/lib/docker/volumes
docker compose start

&lt;span class="c"&gt;# Option B: Use Borg's --ignore-fs-type to skip overlay filesystems&lt;/span&gt;
&lt;span class="c"&gt;# (backs up bind mounts but skips Docker's internal storage)&lt;/span&gt;
borg create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude-if-present&lt;/span&gt; .nobackup &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/docker::&lt;span class="s2"&gt;"{now}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /opt/containers  &lt;span class="c"&gt;# Your compose project directories with bind mounts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4 Gitea Repositories
&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;# Use Gitea's built-in dump command for a consistent backup&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; git gitea dump &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/gitea/app.ini &lt;span class="nt"&gt;--file&lt;/span&gt; /tmp/gitea-dump.zip

&lt;span class="c"&gt;# Then Borg the dump&lt;/span&gt;
borg create &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/gitea::&lt;span class="s2"&gt;"{now}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /tmp/gitea-dump.zip &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/gitea

&lt;span class="c"&gt;# Clean up&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/gitea-dump.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Automation with Cron/Systemd Timers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1 Systemd Timer (Recommended)
&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;# /etc/systemd/system/borg-backup.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Borg Backup Service&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/borgmatic create --stats&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/borgmatic prune --stats&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/borg-backup.timer
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily Borg Backup&lt;/span&gt;

&lt;span class="nn"&gt;[Timer]&lt;/span&gt;
&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true  # Run immediately if a scheduled run was missed&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;300  # Add up to 5 minutes of jitter&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; borg-backup.timer
systemctl list-timers borg-backup.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Cron (Alternative)
&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/cron.d/borg-backup&lt;/span&gt;
&lt;span class="c"&gt;# Run Borg daily at 2:00 AM&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; root /usr/bin/borgmatic create &lt;span class="nt"&gt;--stats&lt;/span&gt; 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; borgmatic
&lt;span class="c"&gt;# Prune old backups weekly&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 0 root /usr/bin/borgmatic prune &lt;span class="nt"&gt;--stats&lt;/span&gt; 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; borgmatic
&lt;span class="c"&gt;# Verify repository integrity monthly&lt;/span&gt;
0 4 1 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; root /usr/bin/borgmatic check &lt;span class="nt"&gt;--progress&lt;/span&gt; 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; borgmatic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Monitoring Backups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8.1 Zabbix Integration
&lt;/h3&gt;

&lt;p&gt;Create a Zabbix user parameter to monitor backup status:&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/zabbix/zabbix_agentd.d/borgbackup.conf&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;borg.last_backup_age,expr &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %Y /var/log/borgmatic/last_success&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;borg.backup_status,cat /var/log/borgmatic/last_status 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;
&lt;span class="nv"&gt;UserParameter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;borg.repo_size,borg info backupuser@backup-server:/backups/proxmox 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Deduplicated size"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a Zabbix trigger that fires if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;borg.last_backup_age&lt;/code&gt; &amp;gt; 86400 (24 hours since last backup)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;borg.backup_status&lt;/code&gt; != "success"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8.2 Ntfy/Telegram Notifications
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Borgmatic ntfy hook:&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="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;after_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -s -X POST https://ntfy.commsnet.org/backups \&lt;/span&gt;
      &lt;span class="s"&gt;-d "✅ Borg backup completed on $(hostname) — $(date)"&lt;/span&gt;
  &lt;span class="na"&gt;on_error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -s -X POST https://ntfy.commsnet.org/backups \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Priority&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high" \&lt;/span&gt;
      &lt;span class="s"&gt;-d "❌ Borg backup FAILED on $(hostname) — $(date)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Restic Telegram notification:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/restic-notify.sh&lt;/span&gt;
&lt;span class="nv"&gt;CHAT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-chat-id"&lt;/span&gt;
&lt;span class="nv"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-bot-token"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MESSAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"✅ Restic backup completed on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;MESSAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"❌ Restic backup FAILED on &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.telegram.org/bot&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sendMessage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"chat_id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CHAT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"text=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MESSAGE&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;h2&gt;
  
  
  9. Testing Restores
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ The most important section of this entire article. An untested backup is NOT a backup.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  9.1 Borg Restore Test
&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;# List available archives&lt;/span&gt;
borg list backupuser@backup-server:/backups/proxmox

&lt;span class="c"&gt;# Mount an archive as a filesystem (read-only)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/restore-test
borg mount backupuser@backup-server:/backups/proxmox /mnt/restore-test

&lt;span class="c"&gt;# Browse and verify files&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /mnt/restore-test/proxmox-2026-07-26T01:46:00/etc/
diff /mnt/restore-test/proxmox-2026-07-26T01:46:00/etc/hosts /etc/hosts

&lt;span class="c"&gt;# Unmount when done&lt;/span&gt;
borg umount /mnt/restore-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9.2 Restic Restore Test
&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;# List snapshots&lt;/span&gt;
restic snapshots

&lt;span class="c"&gt;# Restore a specific snapshot to a temp directory&lt;/span&gt;
restic restore latest &lt;span class="nt"&gt;--target&lt;/span&gt; /tmp/restore-test &lt;span class="nt"&gt;--include&lt;/span&gt; /etc

&lt;span class="c"&gt;# Verify a specific file&lt;/span&gt;
diff /tmp/restore-test/etc/hosts /etc/hosts

&lt;span class="c"&gt;# Or mount and browse&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/restic-mount
restic mount /mnt/restic-mount
&lt;span class="c"&gt;# Browse to /mnt/restic-mount/snapshots/latest/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9.3 Automated Restore Testing
&lt;/h3&gt;

&lt;p&gt;Create a script that tests restores monthly:&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;# /usr/local/bin/test-restore.sh&lt;/span&gt;
&lt;span class="nv"&gt;TEST_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/restore-test-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Restore /etc as a canary&lt;/span&gt;
borg extract &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/proxmox::proxmox-&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="se"&gt;\&lt;/span&gt;
  etc/hosts etc/hostname etc/passwd

&lt;span class="c"&gt;# Verify critical files exist&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in &lt;/span&gt;etc/hosts etc/hostname etc/passwd&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: &lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt; not found in restore"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi
done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Restore test passed"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TEST_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it: &lt;code&gt;0 5 1 * * root /usr/local/bin/test-restore.sh 2&amp;gt;&amp;amp;1 | logger -t restore-test&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Retention Policies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  10.1 Grandfather-Father-Son (GFS)
&lt;/h3&gt;

&lt;p&gt;The classic GFS rotation keeps backups at multiple granularities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Keep&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;One backup per day, keep last 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;One backup per week, keep last 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;One backup per month, keep last 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yearly&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;One backup per year, keep 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This gives you: 7 daily + 4 weekly + 6 monthly + 1 yearly = &lt;strong&gt;18 archives&lt;/strong&gt; total, covering an entire year of recovery points.&lt;/p&gt;

&lt;h3&gt;
  
  
  10.2 Borg Prune
&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;# Prune old archives according to retention policy&lt;/span&gt;
borg prune &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-yearly&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  backupuser@backup-server:/backups/proxmox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in borgmatic:&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;retention&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;keep_daily&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
  &lt;span class="na"&gt;keep_weekly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;keep_monthly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
  &lt;span class="na"&gt;keep_yearly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{hostname}-"&lt;/span&gt;  &lt;span class="c1"&gt;# Only prune archives from this host&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10.3 Restic Forget + Prune
&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;# Mark old snapshots for deletion&lt;/span&gt;
restic forget &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-yearly&lt;/span&gt; 1

&lt;span class="c"&gt;# Actually delete the data (frees space)&lt;/span&gt;
restic prune

&lt;span class="c"&gt;# Or combine with backup command&lt;/span&gt;
restic backup /etc &lt;span class="nt"&gt;--tag&lt;/span&gt; daily
restic forget &lt;span class="nt"&gt;--keep-daily&lt;/span&gt; 7 &lt;span class="nt"&gt;--keep-weekly&lt;/span&gt; 4 &lt;span class="nt"&gt;--keep-monthly&lt;/span&gt; 6 &lt;span class="nt"&gt;--keep-yearly&lt;/span&gt; 1 &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Run &lt;code&gt;restic prune&lt;/code&gt; weekly, not daily. Pruning reorganizes the repository and can be slow on large repos. Forgetting just marks snapshots; pruning actually removes the data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  10.4 Repository Check
&lt;/h3&gt;

&lt;p&gt;Periodically verify your repository integrity:&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;# Borg check (monthly)&lt;/span&gt;
borg check &lt;span class="nt"&gt;--verify-data&lt;/span&gt; backupuser@backup-server:/backups/proxmox

&lt;span class="c"&gt;# Restic check (weekly for metadata, monthly for full data)&lt;/span&gt;
restic check  &lt;span class="c"&gt;# Quick metadata check&lt;/span&gt;
restic check &lt;span class="nt"&gt;--read-data&lt;/span&gt;  &lt;span class="c"&gt;# Full data integrity check (slower)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  11. Conclusion — Summary and Key Takeaways
&lt;/h2&gt;

&lt;p&gt;You now have a robust, automated, monitored, tested backup system. Here's the complete picture:&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Backup Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VM-level&lt;/td&gt;
&lt;td&gt;Proxmox vzdump&lt;/td&gt;
&lt;td&gt;Daily 2AM&lt;/td&gt;
&lt;td&gt;Local storage&lt;/td&gt;
&lt;td&gt;7 daily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-level&lt;/td&gt;
&lt;td&gt;Borg&lt;/td&gt;
&lt;td&gt;Daily 2:30AM&lt;/td&gt;
&lt;td&gt;NAS via SSH&lt;/td&gt;
&lt;td&gt;7d/4w/6m/1y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offsite&lt;/td&gt;
&lt;td&gt;Restic&lt;/td&gt;
&lt;td&gt;Daily 3AM&lt;/td&gt;
&lt;td&gt;S3/B2&lt;/td&gt;
&lt;td&gt;7d/4w/6m/1y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrity&lt;/td&gt;
&lt;td&gt;Borg check&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore test&lt;/td&gt;
&lt;td&gt;Automated script&lt;/td&gt;
&lt;td&gt;Monthly&lt;/td&gt;
&lt;td&gt;/tmp&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate everything&lt;/strong&gt; — borgmatic + systemd timers + resticprofile = hands-off backups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor aggressively&lt;/strong&gt; — Zabbix + ntfy/Telegram notifications mean you know about failures immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test restores regularly&lt;/strong&gt; — mount a backup, verify files, document the process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use retention policies&lt;/strong&gt; — GFS keeps a year of recovery points without consuming infinite storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defense in depth&lt;/strong&gt; — Borg for local (fast restores), Restic for offsite (survives site loss)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your restore process&lt;/strong&gt; — when disaster strikes at 3 AM, you don't want to be reading man pages&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Restore Runbook Template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1. SSH to backup server: ssh backupuser@backup-server
2. List archives: borg list /backups/proxmox
3. Mount archive: borg mount /backups/proxmox::proxmox-2026-07-26 /mnt/restore
4. Copy files: rsync &lt;span class="nt"&gt;-av&lt;/span&gt; /mnt/restore/etc/ /etc/
5. Verify services: systemctl status docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose ps
6. Document what was restored and why
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Useful Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://borgbackup.readthedocs.io/" rel="noopener noreferrer"&gt;Borg Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restic.readthedocs.io/" rel="noopener noreferrer"&gt;Restic Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://torsion.org/borgmatic/" rel="noopener noreferrer"&gt;Borgmatic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://creativeprojects.github.io/resticprofile/" rel="noopener noreferrer"&gt;Resticprofile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy" rel="noopener noreferrer"&gt;3-2-1 Backup Strategy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sleep well knowing your homelab is protected! 🛡️&lt;/p&gt;

</description>
      <category>backup</category>
      <category>homelab</category>
      <category>borg</category>
      <category>restic</category>
    </item>
    <item>
      <title>WireGuard VPN: Secure Remote Access for Your Homelab</title>
      <dc:creator>Luna Commsnet</dc:creator>
      <pubDate>Sat, 22 Aug 2026 11:56:44 +0000</pubDate>
      <link>https://dev.to/lunacommsnet/wireguard-vpn-secure-remote-access-for-your-homelab-ele</link>
      <guid>https://dev.to/lunacommsnet/wireguard-vpn-secure-remote-access-for-your-homelab-ele</guid>
      <description>&lt;h1&gt;
  
  
  WireGuard VPN: Secure Remote Access for Your Homelab
&lt;/h1&gt;




&lt;h2&gt;
  
  
  1. Introduction — Why WireGuard over OpenVPN/IPsec
&lt;/h2&gt;

&lt;p&gt;If you've ever set up a VPN for your homelab, you know the pain of fiddling with OpenVPN configs, dealing with bulky certificate authorities, or wrestling with IPsec's cryptic phase 1/phase 2 negotiations. WireGuard cuts through all that noise.&lt;/p&gt;

&lt;p&gt;WireGuard is a lean, modern VPN protocol that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs in the kernel&lt;/strong&gt; — the Linux implementation is roughly 4,000 lines of code (compared to OpenVPN's 100,000+ and IPsec's 400,000+). This translates to blistering performance and a tiny attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uses state-of-the-art cryptography by default&lt;/strong&gt; — Curve25519 for key exchange, ChaCha20 for symmetric encryption, Poly1305 for authentication, BLAKE2s for hashing. No cipher suites to configure, no deprecated algorithms to accidentally enable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offers simple, deterministic configs&lt;/strong&gt; — a typical peer config is 10-15 lines. Compare that to OpenVPN's 50+ line configs with certificate chains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is cross-platform&lt;/strong&gt; — Linux, macOS, Windows, iOS, and Android all have native, officially-supported clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roams seamlessly&lt;/strong&gt; — switch from WiFi to cellular and your tunnel stays up. WireGuard's connectionless design means there's no "reconnect" delay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a homelab, this means you get enterprise-grade remote access without the enterprise-grade complexity. Whether you're checking on your Home Assistant automations from a coffee shop or pushing config updates to your Proxmox cluster from a hotel, WireGuard gets you there securely with minimal overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  WireGuard vs OpenVPN: Quick Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;WireGuard&lt;/th&gt;
&lt;th&gt;OpenVPN&lt;/th&gt;
&lt;th&gt;IPsec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code size&lt;/td&gt;
&lt;td&gt;~4,000 lines&lt;/td&gt;
&lt;td&gt;~100,000 lines&lt;/td&gt;
&lt;td&gt;~400,000 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto agility&lt;/td&gt;
&lt;td&gt;Fixed (secure defaults)&lt;/td&gt;
&lt;td&gt;Configurable (easy to misconfigure)&lt;/td&gt;
&lt;td&gt;Configurable (easy to misconfigure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Kernel-space, fastest&lt;/td&gt;
&lt;td&gt;User-space, slower&lt;/td&gt;
&lt;td&gt;Kernel-space, fast but complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config complexity&lt;/td&gt;
&lt;td&gt;10-15 lines per peer&lt;/td&gt;
&lt;td&gt;50+ lines + certificates&lt;/td&gt;
&lt;td&gt;30+ lines + phase 1/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key management&lt;/td&gt;
&lt;td&gt;Static keys (simple)&lt;/td&gt;
&lt;td&gt;X.509 PKI (complex)&lt;/td&gt;
&lt;td&gt;IKE/ISAKMP (complex)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UDP support&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Yes (default)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCP fallback&lt;/td&gt;
&lt;td&gt;No (by design)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectionless&lt;/td&gt;
&lt;td&gt;Yes (roams seamlessly)&lt;/td&gt;
&lt;td&gt;No (must reconnect)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditability&lt;/td&gt;
&lt;td&gt;High (small codebase)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low (huge codebase)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note:&lt;/strong&gt; WireGuard's lack of TCP fallback is intentional — if your network blocks outbound UDP, you'll need to address that at the network level (e.g., a VPS relay) rather than trying to tunnel over TCP. TCP-over-TCP is a performance disaster.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Prerequisites
&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;What you need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Debian/Ubuntu (any recent release) with a public IPv4 address OR a Cloudflare Tunnel for port-forwarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client devices&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Linux, macOS, Windows, iOS, Android — any combination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Root access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo&lt;/code&gt; on the server and on each Linux/macOS client (or admin rights on Windows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optional&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A pfSense firewall in front of your server (highly recommended for homelabs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UDP port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;51820 (default) must be reachable on your server's public IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; If your ISP blocks inbound UDP or you're behind CGNAT, spin up a cheap VPS ($3-5/month from Hetzner, Vultr, or Oracle's free tier) and run WireGuard there, then tunnel to your homelab over a WireGuard-to-WireGuard connection. Alternatively, use Cloudflare Tunnel to expose UDP 51820.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Network Planning
&lt;/h3&gt;

&lt;p&gt;Before you start, decide on your VPN subnet. This should NOT overlap with any existing LAN subnet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common VPN subnets&lt;/th&gt;
&lt;th&gt;Avoid if your LAN uses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10.0.0.0/24&lt;/td&gt;
&lt;td&gt;10.0.0.0/24 (obviously)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.100.0.0/24&lt;/td&gt;
&lt;td&gt;Any 10.x.x.x range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;172.16.0.0/24&lt;/td&gt;
&lt;td&gt;172.16.0.0/12 ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.100.0/24&lt;/td&gt;
&lt;td&gt;192.168.x.0/24 ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this guide, I'll use &lt;strong&gt;10.100.0.0/24&lt;/strong&gt; for the VPN subnet and assume your homelab LAN is &lt;strong&gt;192.168.1.0/24&lt;/strong&gt;. Adjust accordingly.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Installing WireGuard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Server (Debian/Ubuntu)
&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;# Update packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="c"&gt;# Install WireGuard (included in kernel since 5.6, tools package provides wg/wg-quick)&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; wireguard

&lt;span class="c"&gt;# Verify the kernel module is loaded&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe wireguard
lsmod | &lt;span class="nb"&gt;grep &lt;/span&gt;wireguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're running a very old kernel (&amp;lt; 5.6), you may need to install &lt;code&gt;wireguard-dkms&lt;/code&gt; instead, which builds the module from source:&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="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; wireguard-dkms wireguard-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Client — Linux
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&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; wireguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Client — macOS (Homebrew or MacPorts)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;wireguard-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download the official macOS app from &lt;a href="https://www.wireguard.com/install/" rel="noopener noreferrer"&gt;https://www.wireguard.com/install/&lt;/a&gt; — it provides a system tray UI for managing tunnels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client — Windows
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the official installer from &lt;a href="https://www.wireguard.com/install/" rel="noopener noreferrer"&gt;https://www.wireguard.com/install/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run the installer — it adds both a GUI (system tray tunnel manager) and the &lt;code&gt;wireguard.exe&lt;/code&gt; CLI utility.&lt;/li&gt;
&lt;li&gt;You can also install via winget: &lt;code&gt;winget install WireGuard.WireGuard&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Client — iOS / Android
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS&lt;/strong&gt;: App Store → search for "WireGuard" (published by WireGuard Development Team)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android&lt;/strong&gt;: Play Store → search for "WireGuard" or F-Droid → "WireGuard"&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; On mobile, use the QR code feature to import configs. Run &lt;code&gt;qrencode -t ansiutf8 &amp;lt; client.conf&lt;/code&gt; on your server to generate a scannable QR code in the terminal.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Server Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Generate Server Keys
&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 directory for WireGuard config and keys&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/wireguard
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;700 /etc/wireguard

&lt;span class="c"&gt;# Generate server private key and derive public key&lt;/span&gt;
&lt;span class="nb"&gt;umask &lt;/span&gt;077
wg genkey | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/wireguard/server_private.key | wg pubkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/wireguard/server_public.key

&lt;span class="c"&gt;# View the keys (you'll need the public key for client configs)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/wireguard/server_public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Warning:&lt;/strong&gt; The private key file must never be committed to version control, shared, or world-readable. The &lt;code&gt;umask 077&lt;/code&gt; ensures only root can read it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.2 Create &lt;code&gt;wg0.conf&lt;/code&gt;
&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;# /etc/wireguard/wg0.conf
&lt;/span&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="c"&gt;# Server private key — keep secret!
&lt;/span&gt;&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;contents of server_private.key&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# VPN interface address
&lt;/span&gt;&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.100.0.1/24&lt;/span&gt;

&lt;span class="c"&gt;# UDP port to listen on
&lt;/span&gt;&lt;span class="py"&gt;ListenPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;

&lt;span class="c"&gt;# MTU — lower if you experience connectivity issues over PPPoE/CGNAT
&lt;/span&gt;&lt;span class="py"&gt;MTU&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1420&lt;/span&gt;

&lt;span class="c"&gt;# Firewall rules: allow forwarding between wg0 and eth0, NAT outbound traffic
# Replace eth0 with your server's actual external interface (check with `ip a`)
&lt;/span&gt;&lt;span class="py"&gt;PostUp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;
&lt;span class="py"&gt;PostDown&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;

&lt;span class="c"&gt;# --- Peers will be added below ---
# [Peer]
# PublicKey = &amp;lt;client_public_key&amp;gt;
# AllowedIPs = 10.100.0.2/32
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; To find your server's external interface name, run &lt;code&gt;ip route get 8.8.8.8 | awk '{print $5; exit}'&lt;/code&gt;. Common names: &lt;code&gt;eth0&lt;/code&gt;, &lt;code&gt;ens3&lt;/code&gt;, &lt;code&gt;enp3s0&lt;/code&gt;, &lt;code&gt;wlan0&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.3 Enable IP Forwarding
&lt;/h3&gt;

&lt;p&gt;Without IP forwarding, your VPN clients can reach the server but not the LAN or internet beyond it:&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;# Enable immediately&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; net.ipv4.ip_forward&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="c"&gt;# Persist across reboots&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"net.ipv4.ip_forward=1"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/sysctl.d/99-wireguard.conf

&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_forward
&lt;span class="c"&gt;# Should output: 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.4 Start the 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;# Enable to start on boot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;wg-quick@wg0

&lt;span class="c"&gt;# Start now&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start wg-quick@wg0

&lt;span class="c"&gt;# Check status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status wg-quick@wg0

&lt;span class="c"&gt;# Verify the interface is up&lt;/span&gt;
ip a show wg0
&lt;span class="c"&gt;# Should show: inet 10.100.0.1/24&lt;/span&gt;

&lt;span class="c"&gt;# Check WireGuard status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Client Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Generate Client Keys (Repeat Per Device)
&lt;/h3&gt;

&lt;p&gt;On your server (or any machine with &lt;code&gt;wg&lt;/code&gt; installed):&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 a directory for client configs&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/wireguard-clients/laptop
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/wireguard-clients/laptop

&lt;span class="c"&gt;# Generate client keys&lt;/span&gt;
&lt;span class="nb"&gt;umask &lt;/span&gt;077
wg genkey | &lt;span class="nb"&gt;tee &lt;/span&gt;laptop_private.key | wg pubkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; laptop_public.key

&lt;span class="c"&gt;# View the public key (needed for server config)&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;laptop_public.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Add the Client as a Peer on the Server
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on the server and append:&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="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="c"&gt;# Laptop
&lt;/span&gt;&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;contents of laptop_public.key&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# Assign this client a static VPN IP
&lt;/span&gt;&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.100.0.2/32&lt;/span&gt;
&lt;span class="c"&gt;# Optional: keepalive for NAT traversal (useful if client is behind NAT)
&lt;/span&gt;&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the change without restarting the tunnel:&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="nb"&gt;sudo &lt;/span&gt;wg syncconf wg0 &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;wg-quick strip wg0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; &lt;code&gt;wg syncconf&lt;/code&gt; applies config changes without dropping existing connections. Use this for adding/removing peers in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5.3 Client Config File
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;laptop.conf&lt;/code&gt; (or use the GUI import on Windows/macOS/iOS):&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/wireguard/wg0.conf (on the client)
&lt;/span&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;contents of laptop_private.key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.100.0.2/24&lt;/span&gt;
&lt;span class="c"&gt;# DNS — use Cloudflare, or your homelab's Unbound/Pi-hole for ad-blocking
&lt;/span&gt;&lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1.1.1.1, 1.0.0.1&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="c"&gt;# The server
&lt;/span&gt;&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;contents of server_public.key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;your-homelab.example.com:51820&lt;/span&gt;
&lt;span class="c"&gt;# Route ALL traffic through the VPN (full tunnel)
&lt;/span&gt;&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0, ::/0&lt;/span&gt;
&lt;span class="c"&gt;# Keepalive — critical if you're behind NAT (maintains the NAT mapping)
&lt;/span&gt;&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 Split Tunnel vs Full Tunnel
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AllowedIPs&lt;/code&gt; on the &lt;strong&gt;client&lt;/strong&gt; config determines what traffic goes through the VPN:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Tunnel (all traffic):&lt;/strong&gt;&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="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0, ::/0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Split Tunnel (only homelab traffic):&lt;/strong&gt;&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;# Only route VPN subnet and homelab LAN through the tunnel
&lt;/span&gt;&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.100.0.0/24, 192.168.1.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Use split tunnel if you only need to access homelab services and want your regular internet traffic to go directly. Use full tunnel if you're on an untrusted network (coffee shop, hotel WiFi) and want all traffic encrypted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5.5 Connect the Client
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Linux:&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="nb"&gt;sudo &lt;/span&gt;wg-quick up wg0
&lt;span class="c"&gt;# Verify&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg show
&lt;span class="c"&gt;# Disconnect&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick down wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;macOS/Windows/iOS/Android:&lt;/strong&gt; Import the config file via the GUI app and toggle the switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.6 Generate QR Code for Mobile Clients
&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 qrencode&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; qrencode

&lt;span class="c"&gt;# Generate QR code in terminal&lt;/span&gt;
qrencode &lt;span class="nt"&gt;-t&lt;/span&gt; ansiutf8 &amp;lt; laptop.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scan with the WireGuard app on iOS or Android — instant config import, no manual typing.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Connecting Your Homelab Services
&lt;/h2&gt;

&lt;p&gt;Once the VPN tunnel is up, your client has a &lt;code&gt;10.100.0.x&lt;/code&gt; address that can reach your homelab. Here's how to access common services:&lt;/p&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;Internal Address&lt;/th&gt;
&lt;th&gt;VPN-Accessible Address&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;Nextcloud&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://nextcloud.commsnet.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same URL (split DNS) or &lt;code&gt;https://10.100.0.1:443&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Works with split DNS or direct IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Assistant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://192.168.1.50:8123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://192.168.1.50:8123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accessible if AllowedIPs includes 192.168.1.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxmox VE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://192.168.1.10:8006&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://192.168.1.10:8006&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Direct access to web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gitea&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://git.commsnet.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same URL via split DNS&lt;/td&gt;
&lt;td&gt;Or &lt;code&gt;https://10.100.0.1:3000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi-hole/Unbound&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.53&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Use as VPN DNS for ad-blocking on the go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Split DNS Setup
&lt;/h3&gt;

&lt;p&gt;If you use domain names like &lt;code&gt;nextcloud.commsnet.org&lt;/code&gt; that resolve to your public IP externally but your private LAN IP internally, you need split DNS. With a VPN, the easiest approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set your VPN DNS to your homelab's DNS server&lt;/strong&gt; (Pi-hole, Unbound, or CoreDNS):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;   &lt;span class="c"&gt;# In client config [Interface] section
&lt;/span&gt;   &lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.1.53&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure your DNS server&lt;/strong&gt; to resolve your domains to internal IPs:

&lt;ul&gt;
&lt;li&gt;Pi-hole: Add local DNS records under Settings → Local DNS&lt;/li&gt;
&lt;li&gt;Unbound: Add &lt;code&gt;local-data&lt;/code&gt; entries in &lt;code&gt;unbound.conf&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This way, when you're on VPN and type &lt;code&gt;nextcloud.commsnet.org&lt;/code&gt;, it resolves to &lt;code&gt;192.168.1.100&lt;/code&gt; (internal) instead of your public IP, keeping traffic inside the tunnel.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Optional: WireGuard Behind Cloudflare Tunnel
&lt;/h2&gt;

&lt;p&gt;If you don't have a static public IP (CGNAT, dynamic IP, or behind a restrictive ISP), Cloudflare Tunnel can expose WireGuard's UDP port securely without opening any inbound ports on your router.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 How It Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Cloudflare Edge (UDP) → Cloudflare Tunnel → Your Server (UDP 51820)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Setup Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Cloudflare Tunnel&lt;/strong&gt; in the Zero Trust dashboard:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://one.dash.cloudflare.com → Networks → Tunnels → Create Tunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install &lt;code&gt;cloudflared&lt;/code&gt;&lt;/strong&gt; on your server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate and run the tunnel:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   cloudflared tunnel login
   cloudflared tunnel create wireguard-relay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure the tunnel to forward UDP 51820:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="c1"&gt;# ~/.cloudflared/config.yml&lt;/span&gt;
   &lt;span class="na"&gt;tunnel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wireguard-relay&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;/root/.cloudflare/wireguard-relay.json&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;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wg.yourdomain.com&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;udp://localhost:51820&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start the tunnel:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update client config&lt;/strong&gt; to point to the tunnel hostname:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;   &lt;span class="nn"&gt;[Peer]&lt;/span&gt;
   &lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;wg.yourdomain.com:51820&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Note:&lt;/strong&gt; Cloudflare Tunnel for UDP is a relatively new feature. Check Cloudflare's documentation for current limitations and supported regions.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8. Security Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8.1 pfSense Firewall Rules
&lt;/h3&gt;

&lt;p&gt;If you run pfSense (or OPNsense) in front of your homelab, create explicit allow rules for WireGuard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WAN Rules (inbound):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Firewall → Rules → WAN&lt;/li&gt;
&lt;li&gt;Add a new rule:

&lt;ul&gt;
&lt;li&gt;Action: Pass&lt;/li&gt;
&lt;li&gt;Interface: WAN&lt;/li&gt;
&lt;li&gt;Address Family: IPv4&lt;/li&gt;
&lt;li&gt;Protocol: UDP&lt;/li&gt;
&lt;li&gt;Source: Any (or restrict to known IPs for extra security)&lt;/li&gt;
&lt;li&gt;Destination: WAN address&lt;/li&gt;
&lt;li&gt;Destination Port: 51820&lt;/li&gt;
&lt;li&gt;Description: "Allow WireGuard VPN inbound"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Place this rule above any default deny rule&lt;/li&gt;
&lt;li&gt;Save and Apply&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;LAN Rules (optional — restrict VPN access to specific services):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Firewall → Rules → LAN&lt;/li&gt;
&lt;li&gt;Add rules to allow traffic from the VPN subnet (10.100.0.0/24) to specific destinations:

&lt;ul&gt;
&lt;li&gt;Allow 10.100.0.0/24 → 192.168.1.50:8123 (Home Assistant)&lt;/li&gt;
&lt;li&gt;Allow 10.100.0.0/24 → 192.168.1.10:8006 (Proxmox)&lt;/li&gt;
&lt;li&gt;Deny 10.100.0.0/24 → 192.168.1.0/24:* (block everything else)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; Following the principle of least privilege, only allow VPN clients to access the specific services they need. Don't blanket-allow the entire LAN unless you trust all VPN users equally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  8.2 Kill Switch (Client Side)
&lt;/h3&gt;

&lt;p&gt;A kill switch prevents traffic from leaking outside the VPN tunnel if the VPN connection drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux (iptables):&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;# Create a kill switch script: /etc/wireguard/killswitch.sh&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;IFACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"wg0"&lt;/span&gt;

&lt;span class="c"&gt;# Flush existing OUTPUT rules (careful if you have other rules!)&lt;/span&gt;
iptables &lt;span class="nt"&gt;-F&lt;/span&gt; OUTPUT

&lt;span class="c"&gt;# Allow traffic through the VPN&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;$IFACE&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Allow established/related connections&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-m&lt;/span&gt; conntrack &lt;span class="nt"&gt;--ctstate&lt;/span&gt; ESTABLISHED,RELATED &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Allow loopback&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-o&lt;/span&gt; lo &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Allow DHCP&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-p&lt;/span&gt; udp &lt;span class="nt"&gt;--dport&lt;/span&gt; 67:68 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Drop everything else&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;macOS (pf):&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;# /etc/pf.anchors/wireguard-killswitch&lt;/span&gt;
&lt;span class="c"&gt;# Block all outbound except through utun (WireGuard interface)&lt;/span&gt;
block out on en0 all
pass out on utun8 all
pass out proto tcp to any port &lt;span class="o"&gt;{&lt;/span&gt; 51820 &lt;span class="o"&gt;}&lt;/span&gt;  &lt;span class="c"&gt;# Allow WireGuard itself&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; The official WireGuard app includes a "Block untunneled traffic (kill switch)" option in the tunnel settings. Enable it.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.3 DNS Leak Prevention
&lt;/h3&gt;

&lt;p&gt;DNS leaks occur when your system queries DNS servers outside the VPN tunnel, potentially revealing your browsing habits to your ISP or network operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prevention strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set VPN DNS in client config:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;   &lt;span class="nn"&gt;[Interface]&lt;/span&gt;
   &lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.1.53  # Your homelab Pi-hole/Unbound&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use systemd-resolved&lt;/strong&gt; (Linux):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# WireGuard integrates with systemd-resolved automatically&lt;/span&gt;
   &lt;span class="c"&gt;# if wg-quick detects it. Verify with:&lt;/span&gt;
   resolvectl status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test for leaks&lt;/strong&gt; at &lt;a href="https://dnsleaktest.com/" rel="noopener noreferrer"&gt;https://dnsleaktest.com/&lt;/a&gt; — all DNS servers shown should be your homelab's DNS, not your ISP's.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  8.4 Key Rotation
&lt;/h3&gt;

&lt;p&gt;WireGuard's static key model means you should rotate keys periodically:&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;# Generate a new key pair&lt;/span&gt;
wg genkey | &lt;span class="nb"&gt;tee &lt;/span&gt;new_client_private.key | wg pubkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; new_client_public.key

&lt;span class="c"&gt;# Update the server config with the new public key&lt;/span&gt;
&lt;span class="c"&gt;# Update the client config with the new private key&lt;/span&gt;

&lt;span class="c"&gt;# Apply&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg syncconf wg0 &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;wg-quick strip wg0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Tip:&lt;/strong&gt; For a homelab with 2-3 clients, manual rotation every 6-12 months is fine. For larger deployments, consider a key management tool like &lt;code&gt;wg-quick&lt;/code&gt; with Ansible or a web UI like WireGuard Portal.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  9. Troubleshooting Common Issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Handshake Never Completes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;wg show&lt;/code&gt; on the client shows &lt;code&gt;latest handshake: (never)&lt;/code&gt; and no data transfer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;How to Check&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UDP port blocked&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo tcpdump -i eth0 udp port 51820&lt;/code&gt; on server&lt;/td&gt;
&lt;td&gt;Open port 51820 on pfSense/router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong endpoint address&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;Endpoint&lt;/code&gt; in client config&lt;/td&gt;
&lt;td&gt;Verify public IP with &lt;code&gt;curl ifconfig.me&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAT not forwarding&lt;/td&gt;
&lt;td&gt;Check router port forwarding rules&lt;/td&gt;
&lt;td&gt;Forward UDP 51820 to server's LAN IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key mismatch&lt;/td&gt;
&lt;td&gt;Compare server's &lt;code&gt;PublicKey&lt;/code&gt; with client config&lt;/td&gt;
&lt;td&gt;Regenerate and re-import keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall on server&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo ufw status&lt;/code&gt; or &lt;code&gt;iptables -L&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Allow UDP 51820: &lt;code&gt;sudo ufw allow 51820/udp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  No Internet After Connecting (Full Tunnel)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; VPN connects (handshake succeeds) but no internet access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Missing NAT/masquerade rule or &lt;code&gt;AllowedIPs&lt;/code&gt; issue.&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 if NAT is working&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-L&lt;/span&gt; POSTROUTING
&lt;span class="c"&gt;# Should show MASQUERADE rule for your interface&lt;/span&gt;

&lt;span class="c"&gt;# If missing, verify wg0.conf PostUp line:&lt;/span&gt;
&lt;span class="c"&gt;# PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;

&lt;span class="c"&gt;# Check IP forwarding&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/sys/net/ipv4/ip_forward
&lt;span class="c"&gt;# Must be 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Can't Access LAN Services (Split Tunnel)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; VPN is up, can reach 10.100.0.1 (server) but not 192.168.1.x (LAN devices).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; &lt;code&gt;AllowedIPs&lt;/code&gt; on client doesn't include the LAN subnet, or server isn't forwarding between interfaces.&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;# Client config — add LAN subnet to AllowedIPs
&lt;/span&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.100.0.0/24, 192.168.1.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also ensure the server's &lt;code&gt;PostUp&lt;/code&gt; rules include forwarding:&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="py"&gt;PostUp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MTU Problems
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; VPN connects but large transfers stall or time out (SSH works, but &lt;code&gt;scp&lt;/code&gt; hangs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Path MTU discovery is broken (common with PPPoE, CGNAT, or nested tunnels).&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;# Add to both server and client [Interface] section:
&lt;/span&gt;&lt;span class="py"&gt;MTU&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1420&lt;/span&gt;

&lt;span class="c"&gt;# If still problematic, try lower:
&lt;/span&gt;&lt;span class="py"&gt;MTU&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1280  # IPv6 minimum, very conservative&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  DNS Queries Leak (Go Through ISP Instead of VPN)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Client DNS not set, or system DNS resolver bypassing WireGuard.&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 which DNS is being used&lt;/span&gt;
resolvectl status  &lt;span class="c"&gt;# systemd-resolved&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/resolv.conf  &lt;span class="c"&gt;# traditional&lt;/span&gt;

&lt;span class="c"&gt;# Fix: set DNS in client config&lt;/span&gt;
&lt;span class="c"&gt;# [Interface]&lt;/span&gt;
&lt;span class="c"&gt;# DNS = 1.1.1.1, 192.168.1.53&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;wg-quick&lt;/code&gt; Fails with "Address already in use"
&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 if the interface is already up&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg show

&lt;span class="c"&gt;# Remove stale interface&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;del wg0

&lt;span class="c"&gt;# Try again&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick up wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Conclusion — Summary and Next Steps
&lt;/h2&gt;

&lt;p&gt;You now have a lightweight, high-performance WireGuard tunnel protecting remote access to your entire homelab. Here's what we covered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Why WireGuard&lt;/strong&gt; — faster, simpler, more secure than OpenVPN/IPsec&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installation&lt;/strong&gt; — on all major platforms (Linux, macOS, Windows, iOS, Android)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server setup&lt;/strong&gt; — key generation, wg0.conf, IP forwarding, systemd service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client setup&lt;/strong&gt; — key generation, peer config, QR code import for mobile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service access&lt;/strong&gt; — connecting to Nextcloud, Home Assistant, Proxmox via VPN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; — for setups without a public IP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security hardening&lt;/strong&gt; — pfSense rules, kill switch, DNS leak prevention, key rotation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Troubleshooting&lt;/strong&gt; — handshake failures, routing, MTU, DNS leaks&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate key distribution:&lt;/strong&gt; Use Ansible or a script to manage client configs across multiple devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up monitoring:&lt;/strong&gt; Monitor VPN connections with Zabbix or Prometheus (track &lt;code&gt;wg show&lt;/code&gt; output)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add 2FA:&lt;/strong&gt; For sensitive homelab services, combine WireGuard with TOTP-based authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-hop:&lt;/strong&gt; If you travel frequently, set up a VPS relay as a WireGuard-to-WireGuard hop for extra anonymity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WireGuard Portal:&lt;/strong&gt; For more than 5 clients, consider running &lt;a href="https://github.com/h44z/wg-portal" rel="noopener noreferrer"&gt;WireGuard Portal&lt;/a&gt; for a web-based user management interface&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Useful Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;WireGuard official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://wiki.archlinux.org/title/WireGuard" rel="noopener noreferrer"&gt;WireGuard on Arch Wiki&lt;/a&gt; (excellent even for non-Arch users)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.pi-hole.net/guides/vpn/wireguard/" rel="noopener noreferrer"&gt;Pi-hole + WireGuard&lt;/a&gt; — ad-blocking VPN combo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.procustodibus.com/" rel="noopener noreferrer"&gt;Pro Custodibus&lt;/a&gt; — WireGuard monitoring and management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy tunneling! 🚀&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>vpn</category>
      <category>homelab</category>
      <category>security</category>
    </item>
    <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;
  
  
  Monitoring Landscape and Why Zabbix Over Prometheus
&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;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 worlds, with Zabbix collection and Grafana visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zabbix has its own dashboards, but they look like 2005. Grafana is the visualization layer that adds beautiful, customizable dashboards, a unified view across multiple data sources, annotation layers (deploy events, maintenance windows), alerting with deduplication and routing, and mobile-responsive layouts so you can check your homelab from your phone.&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;┌────────────────────────────────────────────────────────────────┐
│                    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, here is the layout.&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, with low overhead, fast startup, and a 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. Honestly, I learned this the hard way after initially running Zabbix in a full VM and watching it eat 4GB of RAM for no good reason.&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 and Connect to Zabbix
&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;h4&gt;
  
  
  Connect Grafana to Zabbix
&lt;/h4&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, go to &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 with these settings

&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, pfSense, and Docker Hosts
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Proxmox Host
&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;# 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;h4&gt;
  
  
  pfSense
&lt;/h4&gt;

&lt;p&gt;pfSense has a Zabbix agent package available in Package Manager under Available Packages, called pfSense-zabbix-agent.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;Zabbix&lt;/span&gt; &lt;span class="err"&gt;Server&lt;/span&gt; &lt;span class="py"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.20.10&lt;/span&gt;
&lt;span class="err"&gt;Zabbix&lt;/span&gt; &lt;span class="err"&gt;Server&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;10051&lt;/span&gt;
&lt;span class="py"&gt;Hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pfsense&lt;/span&gt;
&lt;span class="err"&gt;Enable&lt;/span&gt; &lt;span class="err"&gt;active&lt;/span&gt; &lt;span class="py"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yes&lt;/span&gt;
&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;h4&gt;
  
  
  Docker Hosts
&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;# 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 and Customizing Templates
&lt;/h3&gt;

&lt;p&gt;Go to &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;p&gt;&lt;strong&gt;Template Customization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Zabbix templates are good out of the box but need tuning for homelab scale. This surprised me when I first deployed Zabbix, as the defaults were constantly triggering false alarms in my homelab.&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;
  
  
  Dashboards: Infrastructure, pfSense Security, and Proxmox
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Infrastructure Overview
&lt;/h4&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;h4&gt;
  
  
  pfSense Network Security
&lt;/h4&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;h4&gt;
  
  
  Proxmox Virtualization
&lt;/h4&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. I recommend creating variables for host, VLAN, and time interval so you can reuse dashboards flexibly.&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;Variable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$host&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;Query&lt;/span&gt;
  &lt;span class="s"&gt;Query&lt;/span&gt;&lt;span class="na"&gt;: zabbix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hosts*&lt;/span&gt;
  &lt;span class="s"&gt;Multi-value&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yes&lt;/span&gt;
  &lt;span class="s"&gt;Include All&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yes&lt;/span&gt;

&lt;span class="na"&gt;Variable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$vlan&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;Custom&lt;/span&gt;
  &lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MGMT, SERVERS, IOT, GUEST, SERVICES&lt;/span&gt;

&lt;span class="na"&gt;Variable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$interval&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;Interval&lt;/span&gt;
  &lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m,5m,10m,30m,1h,6h,1d&lt;/span&gt;
  &lt;span class="s"&gt;Auto&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Yes&lt;/span&gt;
&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 and Grafana Alerts
&lt;/h3&gt;

&lt;p&gt;The alerting pipeline flows like this:&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, Warning, and Information Alerts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical alerts (wake me up)&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;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;p&gt;&lt;strong&gt;Warning and information alerts&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;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;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; and configure it in Grafana under Alerting, then Contact points, and Add Contact Point.&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;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Telegram&lt;/span&gt;
  &lt;span class="na"&gt;BOT API Token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_BOT_TOKEN&lt;/span&gt;
  &lt;span class="na"&gt;Chat ID&lt;/span&gt;&lt;span class="pi"&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, Database, and Retention
&lt;/h3&gt;

&lt;p&gt;Zabbix's built-in housekeeper is notoriously slow with PostgreSQL. Replace it with partitioned tables using TimescaleDB.&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; since TimescaleDB handles it now.&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/zabbix/zabbix_server.conf
&lt;/span&gt;&lt;span class="py"&gt;DisableHousekeeping&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&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;h4&gt;
  
  
  Zabbix Database Backup
&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;#!/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;h4&gt;
  
  
  Grafana Dashboard Export
&lt;/h4&gt;

&lt;p&gt;Grafana dashboards should be version-controlled. I recommend committing them to git because your dashboards are infrastructure code, not click-and-hope.&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;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 is what your dashboards should look like.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dashboard Visual Descriptions
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure Overview Dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top row&lt;/strong&gt; has 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; shows a time series graph with CPU usage for all hosts over the last 1 hour, with an 80% threshold line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle right&lt;/strong&gt; displays 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; has a network traffic stacked area chart per VLAN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom right&lt;/strong&gt; shows storage usage as a horizontal bar chart per Proxmox pool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;pfSense Security Dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top row&lt;/strong&gt; shows firewall deny rate time series, which should display a consistent low rate. Any spike is suspicious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middle&lt;/strong&gt; has a table of top 10 denied source IPs with last attempt time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bottom&lt;/strong&gt; displays 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 do the following.&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 means wake me, warning means check in morning, info means 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;




&lt;p&gt;&lt;em&gt;This article is also published on:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a href="https://medium.com/@commstech" rel="noopener noreferrer"&gt;Coming soon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/lunacommsnet/self-hosted-monitoring-stack-zabbix-grafana-for-home-infrastructure-53aj"&gt;Self-Hosted Monitoring Stack: Zabbix + Grafana for Home Infrastructure&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://wiki.commsnet.org/en/Blog/Self-Hosted-Monitoring-Stack-Zabbix-Grafana" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;. Follow CommsNet for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sponsor our work: &lt;a href="https://github.com/sponsors/commstech" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>zabbix</category>
      <category>grafana</category>
      <category>monitoring</category>
      <category>homelab</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, like identity providers, microsegmentation platforms, and 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 in my homelab, 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 and Trust Model
&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;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 and pfSense Hardware
&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. I learned this the hard way after burning through two consumer "smart" switches that quietly dropped tagged frames under load.&lt;/p&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. Honestly, if you're running Proxmox already, there's no reason to buy separate firewall hardware unless you need that physical separation for compliance.&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;p&gt;&lt;strong&gt;Management VLAN (Highest Trust)&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;# 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;p&gt;&lt;strong&gt;Servers VLAN&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;# 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;p&gt;&lt;strong&gt;IoT VLAN (Low Trust)&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;# 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. This surprised me when I first started auditing my own IoT traffic, some smart plugs were making hundreds of outbound connections an hour to servers in countries I didn't expect.&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;p&gt;&lt;strong&gt;Guest VLAN (No Trust)&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;# 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;p&gt;&lt;strong&gt;Services VLAN (Exposed Services)&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;# 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 shell"&gt;&lt;code&gt;DENY  IOT_Net    →  RFC1918       //  &lt;span class="k"&gt;*&lt;/span&gt;        //  &lt;span class="s2"&gt;"IOT: Block internal access"&lt;/span&gt;
ALLOW IOT_Net    →  IoT_API_Targets → IoT_Allowed  //  &lt;span class="s2"&gt;"IOT: Allowed services only"&lt;/span&gt;
&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;
  
  
  Verification: Proving Your Segmentation Works
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Test 1: IoT Cannot Reach Management&lt;/strong&gt;&lt;/p&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;p&gt;&lt;strong&gt;Test 2: Guest Cannot Reach Any Internal Host&lt;/strong&gt;&lt;/p&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;p&gt;&lt;strong&gt;Test 3: Servers Cannot Reach Management&lt;/strong&gt;&lt;/p&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;p&gt;&lt;strong&gt;Test 4: pfSense Firewall Logs&lt;/strong&gt;&lt;/p&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 and WiFi Firewall Rules
&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;p&gt;WiFi-specific firewall additions.&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;# 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 and Suricata IDS
&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;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 shell"&gt;&lt;code&gt;Services → Suricata → Interfaces → Add
  Interface: VLAN_30 &lt;span class="o"&gt;(&lt;/span&gt;IoT&lt;span class="o"&gt;)&lt;/span&gt;
  Mode: IPS &lt;span class="o"&gt;(&lt;/span&gt;Inline Prevention&lt;span class="o"&gt;)&lt;/span&gt;
  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 things like IoT devices phoning home to unexpected IPs, malware C2 beacon patterns, and known exploit signatures targeting IoT firmware. In my setup, Suricata caught a cheap WiFi outlet trying to call a Chinese IP every 30 seconds. That device is now quarantined in a separate VLAN with zero internet access.&lt;/p&gt;




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

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

&lt;p&gt;&lt;strong&gt;Mistake 1: Forgetting to Disable VLAN 1&lt;/strong&gt;&lt;/p&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;p&gt;&lt;strong&gt;Mistake 2: Over-Allowing on Trunk Ports&lt;/strong&gt;&lt;/p&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;p&gt;&lt;strong&gt;Mistakes 3 and 4: Floating Rules and Missing Logs&lt;/strong&gt;&lt;/p&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. Avoid floating rules for inter-VLAN policies; use interface-specific rules. Use floating rules only for traffic shaping or QoS.&lt;/p&gt;

&lt;p&gt;If you don't log denied packets, you have no way to detect reconnaissance, failed intrusion attempts, or misconfigured devices. 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 the following.&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;




&lt;p&gt;&lt;em&gt;This article is also published on:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a href="https://medium.com/@commstech" rel="noopener noreferrer"&gt;Coming soon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/lunacommsnet/zero-trust-homelab-network-segmentation-with-vlans-and-pfsense-31ni"&gt;Zero-Trust Homelab: Network Segmentation with VLANs and pfSense&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://wiki.commsnet.org/en/Blog/Zero-Trust-Homelab-Network-Segmentation-VLANs-pfSense" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;. Follow CommsNet for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sponsor our work: &lt;a href="https://github.com/sponsors/commstech" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>vlans</category>
      <category>pfsense</category>
      <category>security</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, plus &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, so no port forwarding is 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 service ports 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 is no longer optional&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, so 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 and DNS Setup
&lt;/h3&gt;

&lt;p&gt;You'll need a domain name (transfer to Cloudflare Registrar or use any registrar with Cloudflare DNS), a Cloudflare account (free tier works for tunnels), and Docker with Docker Compose on your server.&lt;/p&gt;

&lt;p&gt;I learned this the hard way when I first tried self-hosting behind a CGNAT connection. Spent three days fighting port forwarding before realizing it was never going to work. Cloudflare Tunnels solved it in under an hour.&lt;/p&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 conf"&gt;&lt;code&gt;&lt;span class="n"&gt;nextcloud&lt;/span&gt;.&lt;span class="n"&gt;commsnet&lt;/span&gt;.&lt;span class="n"&gt;org&lt;/span&gt;    → &lt;span class="n"&gt;Proxied&lt;/span&gt; (&lt;span class="n"&gt;orange&lt;/span&gt; &lt;span class="n"&gt;cloud&lt;/span&gt;) → &lt;span class="n"&gt;Tunnel&lt;/span&gt;
&lt;span class="n"&gt;hass&lt;/span&gt;.&lt;span class="n"&gt;commsnet&lt;/span&gt;.&lt;span class="n"&gt;org&lt;/span&gt;         → &lt;span class="n"&gt;Proxied&lt;/span&gt; (&lt;span class="n"&gt;orange&lt;/span&gt; &lt;span class="n"&gt;cloud&lt;/span&gt;) → &lt;span class="n"&gt;Tunnel&lt;/span&gt;
&lt;span class="n"&gt;files&lt;/span&gt;.&lt;span class="n"&gt;commsnet&lt;/span&gt;.&lt;span class="n"&gt;org&lt;/span&gt;        → &lt;span class="n"&gt;Proxied&lt;/span&gt; (&lt;span class="n"&gt;orange&lt;/span&gt; &lt;span class="n"&gt;cloud&lt;/span&gt;) → &lt;span class="n"&gt;Tunnel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "Proxied" toggle is critical because 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 and Authenticate 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;p&gt;Authenticate and create your tunnel.&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;# 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;p&gt;Now create &lt;code&gt;~/.cloudflared/config.yml&lt;/code&gt; with your tunnel configuration.&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, Testing, and Service Setup
&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;p&gt;Test the tunnel by running it manually first.&lt;br&gt;
&lt;/p&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. Honestly, the first time I saw this work behind a CGNAT connection, it felt like magic.&lt;/p&gt;

&lt;p&gt;Once confirmed, install it as a systemd 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;# 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 and docker-compose.yml
&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;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 and PHP Tuning
&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;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;p&gt;Notice the two Docker networks in the compose file. &lt;strong&gt;&lt;code&gt;nextcloud-internal&lt;/code&gt;&lt;/strong&gt; has no external routing, so the database and Redis can only be reached by Nextcloud containers. &lt;strong&gt;&lt;code&gt;nextcloud-exposed&lt;/code&gt;&lt;/strong&gt; is where the app container also sits, so Cloudflare Tunnel can reach it on &lt;code&gt;localhost:8080&lt;/code&gt;.&lt;/p&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 Instead of 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;p&gt;Install the required packages first.&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 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;
  
  
  Simpler Alternative (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. This surprised me when I set it up for a family member who just wanted basic automations without the overhead.&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 this 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, so no extra configuration is needed.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Nextcloud Security and Fail2Ban
&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;p&gt;For brute-force protection, fail2ban can ban offending IPs at the firewall level. Here is the configuration.&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 and Cloudflare Access
&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;p&gt;For an extra layer, add Cloudflare Zero Trust authentication &lt;em&gt;before&lt;/em&gt; your service. This is worth the extra setup 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;# 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. I run this on my own homelab and it has been rock solid for months.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Health Checks and Monitoring
&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;
  
  
  Backups
&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;p&gt;For Home Assistant, add automatic backups to &lt;code&gt;configuration.yaml&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 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;
  
  
  Common Issues and Fixes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tunnel won't connect.&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;# 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;p&gt;&lt;strong&gt;Nextcloud shows wrong URL.&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;# 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;p&gt;&lt;strong&gt;Home Assistant not accessible.&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;# 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;p&gt;&lt;strong&gt;Nextcloud performance is slow.&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;# 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;$0&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;$0&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;$0&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; by transferring to Cloudflare or pointing nameservers there&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up Cloudflare Tunnel&lt;/strong&gt; in 15 minutes, zero ports opened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Nextcloud&lt;/strong&gt; with Docker Compose, then configure and migrate your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Home Assistant&lt;/strong&gt; starting small, adding integrations over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up backups&lt;/strong&gt; that are automated, offsite, and tested&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; with Watchtower for updates and 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;




&lt;p&gt;&lt;em&gt;This article is also published on:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a href="https://medium.com/@commstech" rel="noopener noreferrer"&gt;Coming soon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/lunacommsnet/from-isp-lock-in-to-control-self-hosting-nextcloudhome-assistant-behind-cloudflare-tunnels-50p7"&gt;From ISP Lock-in to Control: Self-Hosting Nextcloud Behind Cloudflare Tunnels&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://wiki.commsnet.org/en/Blog/Self-Hosting-Nextcloud-Cloudflare-Tunnels" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;. Follow CommsNet for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sponsor our work: &lt;a href="https://github.com/sponsors/commstech" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextcloud</category>
      <category>cloudflare</category>
      <category>selfhosting</category>
      <category>privacy</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. Honestly, I was surprised how quickly it becomes second nature once you get past the initial learning curve.&lt;/p&gt;

&lt;p&gt;What matters for security is straightforward&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 with pfSense&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, with no sidecar proxies and no iptables chaos. It sees every packet, every connection, every DNS lookup, and it can enforce policy at Layer 3/4 &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;
  
  
  Proxmox Network Configuration
&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. I learned this the hard way when a single trunked NIC failed and took down every VLAN at once during a firmware update.&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;p&gt;Now configure your bridges in &lt;code&gt;/etc/network/interfaces&lt;/code&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;# /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;
  
  
  pfSense VLAN Rules and Aliases
&lt;/h3&gt;

&lt;p&gt;Create pfSense as a VM with at least 2 vCPUs and 2GB RAM. Attach NICs as follows&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;p&gt;This is where most homelabs fall apart. If your IoT devices can reach your NAS, you've already lost. The principle is simple.&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 to 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 to 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 to 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;p&gt;Don't hardcode IPs. Use aliases instead.&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;
  
  
  Cilium Installation and kube-proxy Replacement
&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;p&gt;Running &lt;code&gt;kubeProxyReplacement=strict&lt;/code&gt; means Cilium replaces iptables-based kube-proxy entirely with eBPF. The benefits are significant&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 for Seeing 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. This surprised me the first time I spun it up, I caught a smart plug making DNS requests to a Chinese IP within ten minutes of enabling Hubble.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Policies for 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 for 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, Dashboards, and Alerting
&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;p&gt;These are the key dashboards to build&lt;/p&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;p&gt;Now set up your alerting rules.&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;# 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;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;




&lt;p&gt;&lt;em&gt;This article is also published on:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; &lt;a href="https://medium.com/@commstech" rel="noopener noreferrer"&gt;Coming soon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/lunacommsnet/building-a-secure-homelab-with-proxmox-ve-pfsense-and-cilium-4e8m"&gt;Building a Secure Homelab with Proxmox VE, pfSense, and Cilium&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://wiki.commsnet.org/en/Blog/Building-Secure-Homelab-Proxmox-pfSense-Cilium" rel="noopener noreferrer"&gt;wiki.commsnet.org&lt;/a&gt;. Follow CommsNet for more homelab, networking, and self-hosting content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sponsor our work: &lt;a href="https://github.com/sponsors/commstech" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>proxmox</category>
      <category>pfsense</category>
      <category>security</category>
    </item>
  </channel>
</rss>
