[{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/active/","section":"Tags","summary":"","title":"Active","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"Ai","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/design/","section":"Tags","summary":"","title":"Design","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/github-pages/","section":"Tags","summary":"","title":"Github-Pages","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/","section":"Imad Uddin","summary":"","title":"Imad Uddin","type":"page"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/knowledge-management/","section":"Tags","summary":"","title":"Knowledge-Management","type":"tags"},{"content":"This page is a full tutorial on how this site works — not just what tools were used, but why they exist, what problems they solve, and how to understand them well enough to build your own version from scratch.\nThe core idea: static vs dynamic websites # Before picking any tools, understand the two fundamentally different ways a website can work.\nA dynamic website runs server-side code on every request. When you visit a WordPress blog, a server receives your request, queries a database, runs PHP code, builds the HTML, and sends it back. The page is generated on demand. This gives you flexibility (user logins, comments, personalisation) but adds complexity: you need a server running 24/7, a database, runtime dependencies, and things that can break.\nA static website is just files — HTML, CSS, and JavaScript — that already exist on disk. When a request comes in, the server just returns the file. No code runs, no database is queried. The page was built in advance, once, and the result is what gets served.\nStatic sites are faster (serving a file is trivial), cheaper (no server to maintain), and far simpler to host. The trade-off is that you can\u0026rsquo;t do things that require server-side logic per request. For a personal blog, that trade-off is completely worth it.\nWhat is a static site generator? # Writing HTML by hand for every page is tedious and error-prone. If your navbar changes, you\u0026rsquo;d have to update every single file. Static site generators (SSGs) solve this by letting you write content in a simple format (Markdown), define layouts once, and generate all the HTML automatically.\nThe mental model:\nsource files (Markdown + templates + config) ↓ static site generator ↓ output folder of pure HTML/CSS/JS You write a post in Markdown like this:\n# My Post Title Some content here. This is **bold** and this is *italic*. The SSG takes that, wraps it in your site\u0026rsquo;s layout (navbar, footer, styling), and produces a complete HTML page. Change the layout template once and every page updates.\nHugo is the SSG this site uses. It\u0026rsquo;s written in Go and compiles to a single binary — no package managers, no dependencies, no version conflicts. You download one file, run it, and your site builds in milliseconds.\nThe folder structure, explained # blog/ ├── content/ ← your writing lives here │ ├── posts/ ← blog posts (have dates, sorted chronologically) │ └── projects/ ← projects (evolve over time, no dates shown) ├── config/ │ └── _default/ │ ├── hugo.toml ← core settings (base URL, theme name) │ ├── params.toml ← theme behaviour (dark mode, TOC, hero) │ ├── languages.en.toml ← name, bio, logo, author links │ └── menus.en.toml ← navbar items ├── assets/ │ └── css/ │ └── custom.css ← ALL visual overrides go here ├── themes/ │ └── blowfish/ ← the theme (never edited directly) ├── static/ │ ├── img/ ← images referenced in content │ ├── CNAME ← tells GitHub Pages your custom domain │ └── .nojekyll ← tells GitHub not to run its own build system └── .github/ └── workflows/ └── deploy.yml ← the automated build and deploy pipeline Each folder has a specific job. Hugo knows where to look for each type of file because of established conventions. You don\u0026rsquo;t configure \u0026ldquo;look in content/ for pages\u0026rdquo; — Hugo just does, because that\u0026rsquo;s the convention.\nContent: posts vs projects # Posts have dates that matter. They\u0026rsquo;re events in time — conference recaps, tutorials written at a specific moment, opinions. Hugo sorts them chronologically.\nProjects evolve. A homelab isn\u0026rsquo;t finished on one date. The TAOMM reading notes grow over months. These use showDate: false so the \u0026ldquo;when\u0026rdquo; doesn\u0026rsquo;t matter, just the \u0026ldquo;what\u0026rdquo;.\nThe theme and why you never edit it # Blowfish is the theme — it handles all the HTML templates, CSS framework (Tailwind), and layout logic. It lives in themes/blowfish/ as a git submodule, which means it\u0026rsquo;s a separate Git repository embedded inside this one. When the theme authors release an update, you pull it in without any manual copying.\nThe critical rule: never edit files inside themes/blowfish/. If you do, those edits get overwritten the next time you update the theme. Instead, Hugo has an override system: any file you put in the root layouts/ or assets/ folder takes precedence over the equivalent file in the theme. custom.css works exactly this way — it loads after the theme\u0026rsquo;s CSS and can override anything.\nHow the theme customisation system works # Blowfish uses Tailwind CSS, a utility-first CSS framework. Instead of writing .my-button { padding: 8px; }, Tailwind gives you classes like px-2 that mean the same thing. The HTML has these classes baked in.\nWhen you want to change how the site looks, you have two options:\nConfig params — Blowfish exposes hundreds of settings (colorScheme, showTableOfContents, showRelatedContent, etc.) that you set in params.toml. Always try this first.\nCustom CSS — anything that config can\u0026rsquo;t reach, you override in assets/css/custom.css. You target elements using regular CSS selectors, then overwrite their styles. For example, to make the navbar background a frosted glass blur:\n#menu-blur { background: rgba(8, 8, 8, 0.6) !important; backdrop-filter: blur(12px); } The !important is sometimes needed to beat Tailwind\u0026rsquo;s specificity. This is normal when overriding a utility framework.\nThe background gradient that gives the site its look:\nbody { background: radial-gradient(ellipse at top left, rgba(168, 85, 247, 0.18) 0%, transparent 50%), radial-gradient(ellipse at bottom right, rgba(6, 182, 212, 0.12) 0%, transparent 50%), #080808; background-attachment: fixed; } Two radial gradients layered on a dark base. background-attachment: fixed keeps the gradient anchored to the viewport rather than scrolling with the page.\nSVGs as feature images # Every post and project has a feature image — the card visual that appears in listings. These are hand-coded SVGs.\nWhat is an SVG? SVG stands for Scalable Vector Graphics. Unlike a JPEG or PNG (which store pixel data), an SVG is a text file describing shapes using XML. A circle is \u0026lt;circle cx=\u0026quot;100\u0026quot; cy=\u0026quot;100\u0026quot; r=\u0026quot;50\u0026quot;/\u0026gt; — centre at (100,100), radius 50. Because it describes geometry rather than pixels, it scales to any resolution without blurring.\nA minimal SVG:\n\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; width=\u0026#34;400\u0026#34; height=\u0026#34;200\u0026#34; viewBox=\u0026#34;0 0 400 200\u0026#34;\u0026gt; \u0026lt;rect width=\u0026#34;400\u0026#34; height=\u0026#34;200\u0026#34; fill=\u0026#34;#080808\u0026#34;/\u0026gt; \u0026lt;text x=\u0026#34;200\u0026#34; y=\u0026#34;110\u0026#34; font-family=\u0026#34;monospace\u0026#34; font-size=\u0026#34;24\u0026#34; fill=\u0026#34;#a855f7\u0026#34; text-anchor=\u0026#34;middle\u0026#34;\u0026gt;Hello\u0026lt;/text\u0026gt; \u0026lt;/svg\u0026gt; viewBox defines the internal coordinate system. width and height are the rendered size. These can differ — the SVG scales to fit.\ntext-anchor=\u0026quot;middle\u0026quot; means the x coordinate is the centre of the text, not the left edge. Essential for centering text.\nTo create the glow effects seen on this site\u0026rsquo;s feature images:\n\u0026lt;defs\u0026gt; \u0026lt;filter id=\u0026#34;glow\u0026#34;\u0026gt; \u0026lt;feGaussianBlur stdDeviation=\u0026#34;3\u0026#34; result=\u0026#34;coloredBlur\u0026#34;/\u0026gt; \u0026lt;feMerge\u0026gt; \u0026lt;feMergeNode in=\u0026#34;coloredBlur\u0026#34;/\u0026gt; \u0026lt;feMergeNode in=\u0026#34;SourceGraphic\u0026#34;/\u0026gt; \u0026lt;/feMerge\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;/defs\u0026gt; \u0026lt;text filter=\u0026#34;url(#glow)\u0026#34; ...\u0026gt;BLOG\u0026lt;/text\u0026gt; The filter blurs a copy of the element and composites it behind the original — giving the glow without blurring the sharp version.\nWhat is GitHub Actions? # GitHub Actions is a CI/CD system — Continuous Integration / Continuous Deployment. Those terms sound complex but the idea is simple: automatically run a script every time you push code.\nThe mental model:\nyou push code to GitHub ↓ GitHub detects the push ↓ GitHub spins up a fresh virtual machine ↓ runs your workflow file step by step ↓ result: built files deployed to GitHub Pages The workflow is defined in .github/workflows/deploy.yml. YAML is just a structured text format — indentation means nesting.\nHere\u0026rsquo;s what the deploy workflow does, step by step:\non: push: branches: [main] Trigger: run this whenever someone pushes to the main branch.\n- uses: actions/checkout@v4 with: submodules: true Step 1: check out the repository code onto the virtual machine. submodules: true also pulls in the Blowfish theme submodule — without this, the themes/blowfish/ folder would be empty.\n- name: Setup Hugo env: HUGO_VERSION: \u0026#34;0.162.0\u0026#34; run: | wget -q https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz tar -xzf hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz sudo install hugo /usr/local/bin/ Step 2: download the Hugo binary directly from its GitHub releases. This is more reliable than using a third-party action — no dependency on someone else\u0026rsquo;s code.\n- name: Build run: hugo --minify Step 3: run Hugo. --minify strips whitespace from the output HTML/CSS/JS, making files smaller.\n- uses: actions/upload-pages-artifact@v3 with: path: ./public - uses: actions/deploy-pages@v4 Steps 4–5: take the public/ folder (Hugo\u0026rsquo;s output) and deploy it to GitHub Pages.\nThe whole pipeline runs in about 30 seconds. Every push to main triggers it — you never manually deploy.\nHow DNS and custom domains work # The internet routes traffic using IP addresses — numbers like 185.199.108.153. Humans use domain names like blog.imadinc.com. DNS (Domain Name System) is the global system that translates one to the other. Every domain name maps to one or more DNS records stored on nameservers around the world.\nWhen a browser visits blog.imadinc.com:\nYour device asks a DNS resolver: \u0026ldquo;what\u0026rsquo;s the address for blog.imadinc.com?\u0026rdquo; The resolver looks up the DNS records for imadinc.com (managed in Cloudflare) It finds a CNAME record pointing to syed-imad.github.io It then looks up syed-imad.github.io and gets GitHub\u0026rsquo;s IP addresses Your browser connects to that IP — GitHub\u0026rsquo;s servers — which serve the blog Record types:\nAn A record maps a name directly to an IP address:\nType: A Name: @ Content: 185.199.108.153 (@ means the root domain itself — imadinc.com.)\nA CNAME record maps a name to another name (an alias):\nType: CNAME Name: blog Content: syed-imad.github.io The blog name field creates the subdomain blog.imadinc.com. CNames are preferred for subdomains pointing at services like GitHub, because GitHub\u0026rsquo;s IP addresses can change — they manage that, not you.\nWhy Cloudflare\u0026rsquo;s proxy must be disabled:\nCloudflare sits in front of many domains and can intercept traffic (for caching, DDoS protection, etc.). When the proxy is enabled (orange cloud), traffic goes through Cloudflare\u0026rsquo;s servers and GitHub only ever sees Cloudflare\u0026rsquo;s IP. GitHub can\u0026rsquo;t verify that you own the domain, so it can\u0026rsquo;t issue the HTTPS certificate.\nWith proxy disabled (grey cloud), Cloudflare just answers \u0026ldquo;here\u0026rsquo;s the real address\u0026rdquo; and steps aside. GitHub sees the connection coming from the correct domain, verifies ownership, and issues the certificate automatically.\nThe CNAME file:\nGitHub Pages needs a file called CNAME in the root of the deployed site, containing your domain. Without it, GitHub Pages forgets the custom domain setting when you redeploy. Putting it in static/CNAME means Hugo copies it into every build automatically.\nblog.imadinc.com That\u0026rsquo;s the entire file.\nWriting and publishing content # Every piece of content is a folder containing an index.md file and optionally a feature.svg. The folder name becomes the URL slug.\ncontent/posts/my-post/ ├── index.md ← the content └── feature.svg ← the card image Front matter goes at the top of index.md, between --- markers. This is YAML metadata that Hugo reads:\n--- title: \u0026#34;My Post\u0026#34; date: 2026-06-09 draft: false tags: [\u0026#34;macos\u0026#34;, \u0026#34;security\u0026#34;] summary: \u0026#34;Shown on the card in listings.\u0026#34; --- Your Markdown content goes here. draft: true hides the post on the live site but shows it with hugo server -D locally. Use this while writing.\nTo preview locally before publishing:\nhugo server --source /home/imad/myProjects/blog Hugo watches for file changes and rebuilds instantly. Visit localhost:1313 to see the site exactly as it will appear live.\nTo publish: save the file, then:\ngit add content/posts/my-post/ git commit -m \u0026#34;add post: my post title\u0026#34; git push origin main GitHub Actions takes it from there.\nTech # Hugo — static site generator Blowfish — Hugo theme GitHub Actions — automated build and deploy GitHub Pages — hosting Cloudflare — DNS and domain management Repo: github.com/Syed-Imad/Syed-Imad.github.io Actively maintained. New posts added as things worth writing about happen.\n","date":"9 June 2026","externalUrl":null,"permalink":"/projects/personal-blog/","section":"Projects","summary":"A full walkthrough of how this site was built — static site generators, GitHub Actions, DNS, SVGs, and custom domains explained from first principles so you can build and understand one yourself.","title":"Personal Blog","type":"projects"},{"content":"This page explains the second brain system this blog uses — not just what it is, but why the problem exists, how the solution works at a technical level, and how you could build your own version from scratch.\nThe problem: AI conversations are ephemeral # Every conversation with an AI starts cold. The model has no memory of previous sessions. You explain who you are, what you\u0026rsquo;re working on, and what you already know — and then next time, you do it again. Over dozens of sessions, this re-introduction overhead compounds into something genuinely frustrating.\nThere\u0026rsquo;s a second problem: the conversations themselves contain valuable learning. If you\u0026rsquo;ve just spent an hour working through how DNS records work, the model\u0026rsquo;s explanation is sitting there in your chat history, mixed in with all the noise, and will eventually vanish. None of it gets retained anywhere useful.\nA second brain is a system for solving both problems:\nGive the AI standing context — so it knows who you are before you say a word Capture knowledge as it happens — so conversations get distilled into permanent notes What Claude Code is, and how its context system works # Claude Code is an AI assistant that runs as a command-line interface (CLI) — a program you run in a terminal. Unlike the web chat interface, it has access to your filesystem. It can read files, write files, run shell commands, and search your codebase.\nThe key feature for this system: CLAUDE.md.\nWhen Claude Code starts in a directory, it looks for a file called CLAUDE.md and reads it before doing anything else. This file is plain text — you write instructions in it, and the AI treats them as standing rules for every session. It\u0026rsquo;s the equivalent of briefing a new colleague every morning, except you only write the brief once.\nThe CLAUDE.md for this project contains instructions like:\n## At the start of every session 1. Check the inbox first — run `ls inbox/` and list any files 2. If inbox has files — process each one before doing anything else 3. Read `brain/index.md` to get oriented 4. Read any brain files relevant to the current task Claude Code reads this and follows those steps automatically. You don\u0026rsquo;t remind it. You don\u0026rsquo;t re-explain. The context is always there.\nSession memory files go a step further. The memory/ folder contains concise .md files that get loaded into every conversation automatically. Think of them as the AI\u0026rsquo;s working memory — short, structured notes about who you are, what you\u0026rsquo;re building, and how you want to collaborate.\nThe folder structure # ghNotes/ ├── CLAUDE.md ← standing instructions for every session ├── inbox/ ← drop raw files here (notes, articles, anything) ├── archive/ ← processed inbox files (date-prefixed) ├── brain/ │ ├── index.md ← read first every session — navigation hub │ ├── about-me.md ← background, goals, preferences │ ├── projects/ ← one file per ongoing project │ └── topics/ ← domain knowledge and skills └── memory/ ← concise session-level pointers Each folder has a distinct role:\ninbox/ is the input. When you learn something, read something, or want to remember a conversation, you write a raw .md file and drop it here. It doesn\u0026rsquo;t need to be formatted — just captured. The AI processes it at the start of the next session.\nbrain/ is the output. Distilled, structured notes. The AI reads these to understand your context. You write to them when something important happens.\narchive/ is processed history. Once the AI reads an inbox file and extracts the useful parts into the brain, it moves the original to archive/ with a date prefix: 2026-06-09_dns-notes.md. You can refer back to originals if needed, but they\u0026rsquo;re out of the active path.\nmemory/ is the fastest-loading layer. These are short, one-concept-per-file notes that get loaded every session automatically. The brain has depth; memory has speed.\nThe inbox-to-brain workflow # This is the core loop:\n1. You learn something or want to capture a conversation 2. Write a raw note, paste an article, or export a chat to inbox/ 3. Next Claude Code session: AI reads inbox/, extracts what matters 4. AI merges it into the right brain file (topics/ or projects/) 5. AI moves the raw file to archive/ with a date prefix 6. You start from richer context next time The inbox means you never need to be disciplined about when to take notes. Just drop whatever you have, in whatever state it\u0026rsquo;s in. The AI handles the distillation.\nThe brain files are written in a specific style: dense and structured, no padding. The goal is high signal per line — bullet points, examples, concrete facts. The AI that reads these files needs to extract context quickly, not wade through prose.\nA good brain file entry looks like this:\n## DNS record types ### CNAME record Alias — maps a name to another name, not an IP. Use for subdomains pointing at services (GitHub, Vercel) because their IPs change. Example: `blog → syed-imad.github.io` creates `blog.imadinc.com` The resolution chain: blog.imadinc.com → syed-imad.github.io → GitHub\u0026#39;s IP → site Contrast with what you\u0026rsquo;d write in a chat message or casual note. The brain file strips all conversational scaffolding and keeps only the reusable knowledge.\nWhy Git and Markdown as the storage format # This system stores everything as plain text .md files in a Git repository. That choice is deliberate.\nMarkdown is just text with lightweight formatting (headers with #, bold with **, lists with -). Any text editor can read it. Any AI can read it. It will be readable in twenty years.\nGit tracks every change with a timestamp and a message. Every time the brain files are updated, a commit records what changed, when, and why. You get a complete history of your learning — not just the current state, but how it evolved.\nThe alternative is a note-taking app (Notion, Obsidian, Bear). These are great tools, but they have lock-in: your notes are stored in a proprietary format or database. If the app disappears or changes its pricing, your notes are hard to migrate. A Git repo with Markdown files can be opened, read, and searched with any tool, forever.\nTo understand what Git is tracking:\ngit log --oneline -10 Shows the last 10 commits — each one a snapshot of the brain at a point in time.\ngit diff HEAD~1 Shows what changed since the previous commit.\nBuilding your own # You can replicate this system in about 15 minutes:\n1. Create the repo:\nmkdir second-brain \u0026amp;\u0026amp; cd second-brain git init mkdir -p inbox archive brain/projects brain/topics memory touch brain/index.md brain/about-me.md 2. Write a CLAUDE.md:\nThe key sections: what to do at session start, how to process inbox files, where to store different types of knowledge, and your preferred writing style. Be specific — the AI follows exactly what you write.\n## At the start of every session 1. Run `ls inbox/` — process any files before anything else 2. Read `brain/index.md` to orient yourself ## Inbox processing - Personal background → merge into `brain/about-me.md` - Project work → create/update `brain/projects/\u0026lt;name\u0026gt;.md` - Technical topic → create/update `brain/topics/\u0026lt;name\u0026gt;.md` - After processing: move to `archive/YYYY-MM-DD_filename.md` 3. Write a first brain file:\nStart with brain/about-me.md. Write your background, what you\u0026rsquo;re working on, and how you learn best. This is what the AI reads to understand who it\u0026rsquo;s talking to.\n4. Push to GitHub:\ngit remote add origin https://github.com/yourusername/second-brain.git git push -u origin main 5. Start using the inbox:\nNext time you have something worth keeping — a good explanation from an AI, notes from a course, a decision you made on a project — drop a .md file in inbox/. Open Claude Code at the repo root the next day. The rest happens automatically.\nWhat compounds over time # The reason this system gets more valuable the longer you use it: each session starts from the accumulated context of every previous session. An AI that knows your background, your current projects, your learning preferences, and your existing knowledge of a topic can give you much better help than one starting cold.\nThe brain grows incrementally. A week in, it knows your current projects. A month in, it knows your patterns and preferences. Six months in, it\u0026rsquo;s a genuine record of your learning across everything you\u0026rsquo;ve worked on.\nTech # ghNotes — the GitHub repo (private) Claude Code — reads and writes to it each session Markdown + Git — plain files, fully version controlled Claude.ai Projects — upload brain/ to the web app for context outside the terminal Actively maintained. The brain grows with every session.\n","date":"9 June 2026","externalUrl":null,"permalink":"/projects/second-brain/","section":"Projects","summary":"A full tutorial on building a persistent AI knowledge system — what the problem is, how Claude Code’s instruction system works, and how to structure a knowledge base that actually compounds over time.","title":"Second Brain","type":"projects"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/tools/","section":"Tags","summary":"","title":"Tools","type":"tags"},{"content":"","date":"9 June 2026","externalUrl":null,"permalink":"/tags/web/","section":"Tags","summary":"","title":"Web","type":"tags"},{"content":"I\u0026rsquo;m Imad — a third-year Cybersecurity student at the University of Warwick, currently interning at CrowdStrike on the GTAC eCrime team working on threat intelligence and adversary tracking.\nI\u0026rsquo;m particularly interested in Apple platform security (macOS/iOS), threat detection, and building things in my homelab. I attended Objective for the We (OFTW) v3 in London in 2025, which pulled me deeper into the macOS security research community.\nThis blog is where I write about things I\u0026rsquo;m genuinely learning: security research, tools I\u0026rsquo;m building, events I attend, and concepts I want to understand deeply enough to explain clearly.\nI\u0026rsquo;m also a hobbyist photographer.\nFeel free to reach out via LinkedIn or email.\n","date":"7 June 2026","externalUrl":null,"permalink":"/about/","section":"Imad Uddin","summary":"","title":"About","type":"page"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/apple-security/","section":"Tags","summary":"","title":"Apple-Security","type":"tags"},{"content":"VM 101 — IP: 192.168.5.127\nThe arr stack is a suite of open-source tools that, together, make downloading and organising media completely automatic. You add a film to your watchlist, and it appears in Plex — formatted, renamed, subtitled — without touching anything. This page explains every component, why it runs in a VM instead of an LXC, and the two key concepts (VPN kill switch and hardlinks) that make the whole system work correctly.\nWhy a VM instead of an LXC # Every other homelab service runs in an LXC container. The media server runs in a VM. The reason: Docker.\nDocker is an application container runtime. Running Docker inside an LXC creates a nested container situation (LXC container → Docker daemon → Docker containers) that causes complications with networking namespaces, cgroup hierarchies, and storage drivers. It works, but it\u0026rsquo;s fragile.\nA VM provides a clean, isolated Ubuntu Server installation with its own kernel, dedicated to running Docker. The Docker daemon has full control of its environment. If the media server VM breaks, qm destroy 101 from Proxmox and rebuild — no impact on other services.\nVM spec: 4 vCPUs, 8GB RAM, 50GB local disk (OS only). Media files live on TrueNAS via NFS (MediaPool dataset).\nThe pipeline # You add something to Plex Watchlist ↓ Overseerr sees the watchlist request ↓ Radarr (movies) or Sonarr (TV) picks it up ↓ Prowlarr searches configured torrent indexers ↓ Best release selected based on quality profile ↓ qBittorrent downloads it, through Gluetun VPN tunnel ↓ Download completes → Radarr renames + hardlinks to media library ↓ Radarr notifies Plex → Plex scans → appears in library Everything is API-driven. Radarr doesn\u0026rsquo;t click buttons in qBittorrent — it sends API calls. You wire the services together once during setup. After that, it\u0026rsquo;s automatic.\nEvery service # Service Port Job Plex 32400 Media server — the Netflix-like streaming interface Radarr 7878 Movie manager — finds, downloads, renames, upgrades movies Sonarr 8989 TV manager — same as Radarr for seasons/episodes Prowlarr 9696 Indexer manager — searches torrent sites, syncs to arr apps qBittorrent 8080 Download client — torrent peer connections, file transfers Gluetun — VPN gateway — WireGuard tunnel, all qBT traffic goes through it Bazarr 6767 Subtitle fetcher — OpenSubtitles, auto-synced to downloaded media Overseerr 5055 Request portal — search and request, Plex Watchlist integration Lidarr 8686 Music manager — same pattern as Radarr for music FlareSolverr 8191 Cloudflare bypass — lets Prowlarr access CF-protected indexers VPN kill switch # When you join a torrent swarm, your IP address is visible to every peer — including copyright monitoring agencies that seed popular torrents specifically to harvest IPs. Your ISP can also see you\u0026rsquo;re torrenting. The VPN ensures peers and your ISP see the VPN provider\u0026rsquo;s IP, not yours.\nThe kill switch mechanism:\nIn Docker Compose, each service has its own network namespace by default. qBittorrent is configured differently:\nqbittorrent: network_mode: \u0026#34;service:gluetun\u0026#34; network_mode: \u0026quot;service:gluetun\u0026quot; means qBittorrent has no network namespace of its own — it shares Gluetun\u0026rsquo;s. All of qBittorrent\u0026rsquo;s traffic — downloads, peer connections, the web UI — flows through the Gluetun container\u0026rsquo;s WireGuard tunnel.\nIf the VPN drops, Gluetun\u0026rsquo;s network disappears. qBittorrent immediately loses all internet connectivity. No fallback, no leaks. The kill switch is structural, not a configuration option that could accidentally be disabled.\nGluetun needs cap_add: NET_ADMIN to create the WireGuard tunnel interface (a privileged kernel operation).\nVPN and download speeds: Surfshark (the current VPN) doesn\u0026rsquo;t support port forwarding. Without port forwarding, peers can\u0026rsquo;t connect to you directly — you can only initiate connections, which limits the number of peers and slows downloads especially for rare content.\nVPN type Speed 10GB file No port forwarding (Surfshark) 2–5 MB/s 30–80 min Port forwarding (ProtonVPN, AirVPN) 5–15 MB/s 10–30 min Private trackers Full connection speed 3–10 min Better VPNs for torrenting: ProtonVPN (Swiss jurisdiction, port forwarding on paid plans), AirVPN (built specifically for torrenting, port forwarding free), Mullvad (anonymous signup, port forwarding).\nHardlinks: zero-space duplication # When Radarr \u0026ldquo;moves\u0026rdquo; a completed download to the media library, it doesn\u0026rsquo;t copy the file. It creates a hardlink — a second directory entry pointing to the same inode (the actual data blocks on disk).\n/data/downloads/complete/Film.2024.mkv ─┐ ├─ same disk blocks, same inode /data/media/movies/Film (2024)/Film.mkv ─┘ Two file paths. One copy of the data. Zero extra disk space used.\nThis matters because qBittorrent needs to keep the file available to continue seeding. If Radarr copied the file and deleted the original, seeding stops. With a hardlink, both paths exist and point to the same data — qBittorrent seeds from its location, Plex serves from the library location, no space wasted.\nThe critical rule: same filesystem # Hardlinks only work within the same filesystem. You cannot hardlink across two different ZFS datasets, two different drives, or two different mount points that happen to be on different underlying filesystems.\nThis is why the Docker Compose volume mounts are:\nradarr: volumes: - /data:/data # NOT /data/media:/media, /data/downloads:/downloads qbittorrent: volumes: - /data:/data # same /data root — hardlinks work Every container uses /data as the root. Movies, TV, and downloads are subdirectories:\n/data/media/movies/ /data/media/tv/ /data/downloads/complete/ /data/downloads/incomplete/ Same root, same filesystem, hardlinks work. If you mount them separately (/data/media and /data/downloads as different volumes), they\u0026rsquo;d be different filesystem namespaces in the containers even if they\u0026rsquo;re the same filesystem on the host — hardlinks fail.\nZFS dataset implication: MediaPool is a single ZFS dataset. Subdirectories (movies/, tv/, downloads/) are just folders inside it — same ZFS filesystem. If you created separate datasets for movies and downloads, hardlinks would fail between them. One dataset, folders for organisation.\nDocker networking: how services find each other # Docker containers on the same host (using the default bridge network) can resolve each other by service name, not IP address:\nhttp://radarr:7878 http://sonarr:8989 http://prowlarr:9696 No IP management, no ports to remember. Services are just named.\nThe qBittorrent routing pattern: qBittorrent shares Gluetun\u0026rsquo;s network namespace. To reach qBittorrent from other containers, you route through the Gluetun container name:\nRadarr → http://gluetun:8080 → qBittorrent (inside Gluetun\u0026#39;s namespace) If you tried http://qbittorrent:8080 from Radarr, it would fail — qBittorrent doesn\u0026rsquo;t have its own container name on the Docker network because it has no independent network namespace.\nThe Plex exception: Plex uses network_mode: host — it needs to broadcast on the LAN for local Plex client discovery. Host networking means Plex is on the VM\u0026rsquo;s network directly, not Docker\u0026rsquo;s internal bridge.\nConsequence: you can\u0026rsquo;t reach Plex by container name from other Docker services. Use the VM\u0026rsquo;s actual IP:\nRadarr → Connect to Plex → http://192.168.5.127:32400 Quality profiles # A quality profile is a ranked list of acceptable release formats. Radarr and Sonarr parse release names (Film.2024.1080p.BluRay.x265-GROUP) to know what they\u0026rsquo;re getting.\nSource quality (best to worst): BluRay \u0026gt; WEB-DL \u0026gt; WEBRip \u0026gt; HDTV\nRecommended setup:\n1080p WEB-DL as the target — grabbed directly from streaming services, excellent quality, 8–15GB (vs 20–50GB for BluRay remux) Disable 4K — files are enormous and the performance hit on streaming clients is real Upgrades Allowed: on — if a better copy of something you already have appears, it downloads automatically and replaces the lower quality version Connecting services (initial setup) # Every arr app exposes an API. Radarr doesn\u0026rsquo;t interact with qBittorrent through a UI — it sends API calls. You connect services by entering each app\u0026rsquo;s address and API key in the other app\u0026rsquo;s Settings:\nIn Radarr Settings → Download Clients → Add qBittorrent:\nHost: gluetun (container name, routes through to qBittorrent) Port: 8080 In Radarr Settings → Connect → Add Plex:\nHost: 192.168.5.127 (VM IP, because Plex uses host networking) Port: 32400 In Prowlarr → Settings → Apps → Add Radarr/Sonarr:\nProwlarr URL: http://prowlarr:9696 Radarr URL: http://radarr:7878 Prowlarr syncs all configured indexers to Radarr/Sonarr automatically Troubleshooting quick reference # Problem Fix qBittorrent first-login password docker logs qbittorrent → look for temp password line Prowlarr DNS errors Add dns: [1.1.1.1, 8.8.8.8] to prowlarr in compose (Gluetun blocks external DNS) Plex claim token expired New token at plex.tv/claim → update compose env → docker compose up --force-recreate plex Files not appearing in Plex Add Plex connection in Radarr/Sonarr Settings → Connect Check VPN is working docker exec gluetun wget -qO- ifconfig.io — should return a different IP from home Update all containers docker compose pull \u0026amp;\u0026amp; docker compose up -d ","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/arr-stack/","section":"Projects","summary":"A fully automated media pipeline — Plex, Radarr, Sonarr, Prowlarr, and qBittorrent behind a VPN kill switch. Covers how the arr stack works, why hardlinks save disk space, and how Docker networking connects it all.","title":"Arr Stack — Media Server","type":"projects"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"This page covers the foundational concepts behind the homelab — the things every other service page assumes you understand. If a service page mentions LXC, ZFS, iSCSI, or double NAT without explaining them, this is where to look.\nWhat a homelab is and why # A homelab is a server (or set of servers) that you run yourself, at home, on hardware you control. No cloud provider, no subscription, no abstracted infrastructure.\nThe point isn\u0026rsquo;t to save money — cloud services are often cheaper. The point is that when something breaks in a homelab, you have to understand it properly to fix it. There\u0026rsquo;s no support ticket. You read the logs, trace the problem, identify which layer is failing, and fix it yourself.\nCloud providers abstract away everything that matters: storage pools, hypervisor config, network routing, certificate management. A homelab makes all of it visible.\nProxmox: the hypervisor # When you have a powerful server, you run many isolated environments on it simultaneously. The software that makes this possible is a hypervisor.\nProxmox VE runs directly on the R630\u0026rsquo;s bare metal — there\u0026rsquo;s no Windows or Ubuntu underneath it. When you boot the server, Proxmox starts. Everything else runs inside Proxmox as VMs or containers.\nVMs vs LXC containers: the key architectural decision # This is the most important concept in the homelab. Understanding it properly lets you make the right choice for every service.\nThe question: how does the OS guarantee that process A can\u0026rsquo;t see or affect process B?\nVirtual Machines # A VM runs an entirely separate kernel. Proxmox emulates hardware — a virtual CPU, virtual RAM, virtual network card. The guest OS (e.g. Ubuntu) boots inside this emulated hardware and loads its own kernel. From the guest\u0026rsquo;s perspective it\u0026rsquo;s running on a real machine.\nResult: complete isolation. Guest kernel can be different from the host kernel. A vulnerability in the guest can\u0026rsquo;t escape to the host without a hypervisor exploit. Cost: full OS overhead — multiple gigabytes of disk, hundreds of megabytes of RAM, seconds of boot time.\nLXC Containers # LXC containers share the host kernel but use two kernel features to create isolation:\nNamespaces — the kernel gives each container its own view of the world:\nPID namespace: PID 1 inside the container is e.g. PID 4823 on the host Network namespace: each container has its own interfaces and routing tables Mount namespace: each container sees its own filesystem root Cgroups (control groups) — resource accounting. \u0026ldquo;This container can use at most 2 cores and 4GB RAM.\u0026rdquo; Without cgroups, a runaway process in one container could starve the host.\nVMs: [Host OS + Hypervisor] │ ┌────┴─────┐ │ Guest OS │ ← full kernel, own init, own filesystem │ Ubuntu │ ← 2GB+ RAM overhead └──────────┘ LXC: [Host OS Kernel — shared] │ ┌────┴──────┐ │ LXC │ ← separate namespace, cgroup limits │ (process) │ ← ~50MB RAM overhead └───────────┘ Rule of thumb: LXC for infrastructure services (database, reverse proxy, VPN gateway). VM for running Docker (where you want the Docker daemon fully isolated, not nested in an LXC).\nUnprivileged LXCs and UID mapping # Proxmox creates LXCs as unprivileged by default. Inside an unprivileged LXC, the root user appears to be UID 0 — but on the host it\u0026rsquo;s actually UID 100000. If that \u0026ldquo;root\u0026rdquo; escaped the container namespace, it would be an unprivileged user on the host, not actual root.\nThis creates a practical problem with NFS mounts: NFS authentication is UID-based. LXC root (host UID 100000) doesn\u0026rsquo;t match the NFS server\u0026rsquo;s expected UIDs. The fix: mount NFS on the Proxmox host, then pass the folder into the LXC via a bind mount. The LXC sees a directory, not a network mount — UID mapping becomes irrelevant.\nThis is one reason most services here use iSCSI block storage instead of NFS.\nTrueNAS and ZFS: how storage works # TrueNAS runs on the R730 and provides all persistent storage. The important part is ZFS — the filesystem underneath TrueNAS.\nWhy ZFS exists # Traditional filesystems (ext4, NTFS) overwrite data in-place. If a write is interrupted (power failure, kernel panic), the file can be left corrupt — part old data, part new. ZFS solves this with two mechanisms:\nCopy-on-write (COW): ZFS never overwrites. When you modify a file, ZFS writes the new version to empty space, updates a pointer to point at the new location, then marks the old space free. If the write is interrupted, the pointer hasn\u0026rsquo;t been updated — the old version is still intact. The partial new write is orphaned data that ZFS discards.\nChecksums: Every block stored in ZFS has a cryptographic checksum stored separately (in the parent tree, not next to the data). When ZFS reads a block, it checksums what it read and compares to the stored checksum. If they don\u0026rsquo;t match, the data has been silently corrupted — and ZFS knows. It can recover from a mirror. Traditional filesystems would silently return the corrupt data without knowing.\nSnapshots: Because ZFS never overwrites, taking a snapshot is instantaneous. A snapshot is just a frozen pointer to the current state — no data is copied. As files change after the snapshot, the old versions remain accessible through it.\nPools, datasets, and zvols # Pool — the collection of physical drives. ZFS manages redundancy at this level. Example: MyMassStorage.\nDataset — a mounted filesystem within the pool. Like a folder with its own quota, compression, snapshot schedule. Can be shared over NFS or SMB. You browse it as a directory tree.\nZvol — a raw block device within the pool. Has a fixed size. Exported via iSCSI. You format it yourself (with ext4). The consumer (a Proxmox LXC) sees it as a physical disk.\nThe key distinction:\nDataset → use when you want a browsable filesystem (backups, media files, Obsidian vault) Zvol → use when the app needs block storage (databases, anything using file locking, Nextcloud data directory) Networking: from packets to services # Subnets and addressing # Every device on the homelab network has an IP in 192.168.5.0/24. The /24 means the first 24 bits are fixed (192.168.5) and the last 8 bits identify the device — 254 usable addresses. The gateway (192.168.5.1, the Linksys router) is the exit point for all traffic leaving the subnet.\nDouble NAT # This homelab sits behind two routers. The Virgin Media hub does its own NAT, then the Linksys does its own NAT behind it. Traffic from the internet arrives at the Virgin Media hub (public IP 82.4.94.229), which forwards to the Linksys, which forwards to the R630.\nFor port forwarding to work, both routers need forwarding rules. Port 80 and 443 are forwarded all the way through to NPM (LXC 104 at 192.168.5.109).\nCloudflare wildcard DNS # Rather than creating a DNS record per subdomain, one wildcard record covers everything:\nType: A Name: *.imadinc.com Value: 82.4.94.229 Proxy: orange (proxied) Every subdomain (nextcloud.imadinc.com, plex.imadinc.com, etc.) resolves to the home IP automatically. The wildcard means adding a new service requires zero DNS changes — just configure NPM to route the new domain to the right internal IP.\nCloudflare proxied mode: Cloudflare terminates the connection at its edge and opens a new one to the origin. This hides the home IP and provides DDoS protection. The downside: Let\u0026rsquo;s Encrypt\u0026rsquo;s standard HTTP challenge fails (Cloudflare answers instead of NPM). All certificates use the DNS challenge instead — see the NPM page for details.\niSCSI: block storage over the network # iSCSI (Internet Small Computer Systems Interface) takes SCSI commands — the standard language computers use to talk to storage devices — and sends them over TCP/IP instead of a physical cable.\nWhen you plug a drive via SATA, the computer sends SCSI commands down the cable. iSCSI sends the same commands over the LAN. TrueNAS holds the storage. Proxmox LXCs are the consumers.\nFrom the LXC\u0026rsquo;s perspective: identical to a locally attached disk. It appears as /dev/sdb. The LXC has no idea it\u0026rsquo;s talking to network storage. This is why file locking works correctly — the application thinks it\u0026rsquo;s using a local disk, because as far as POSIX semantics go, it is.\nThe 5 TrueNAS iSCSI components # All five must exist and be correctly linked:\nComponent What it is Portal Network endpoint — IP and port TrueNAS listens on (0.0.0.0:3260) Initiator Group Which clients may connect — contains initiator IQNs Authorized Access Authentication — open access on a private LAN Extent The actual storage — points at a zvol Target The named iSCSI object — links Portal + Initiator Group + Extent IQNs — the thing that trips everyone up # Every iSCSI device has an IQN (iSCSI Qualified Name). Format:\niqn.YYYY-MM.reverse-domain:identifier TrueNAS matches initiators by IQN, not by IP address. The Proxmox initiator IQN is auto-generated:\ncat /etc/iscsi/initiatorname.iscsi # InitiatorName=iqn.1993-08.org.debian:01:e7dd6e7909d The most common mistake: putting 192.168.5.146 (the Proxmox IP) in the Initiator Group\u0026rsquo;s \u0026ldquo;Allowed Initiators\u0026rdquo; field. TrueNAS ignores IPs there. The connection succeeds at TCP level, but TrueNAS returns zero targets because no IQN matched:\niscsiadm: discovery session to 192.168.5.142:3260 received text response, 0 data bytes Zero bytes = connection worked, but no matching initiator found. Fix: delete the initiator group, recreate with the actual Proxmox IQN.\nCorrect setup order # Must be built in this sequence:\nCreate Portal → note Portal Group ID Create Initiator Group → paste Proxmox IQN → note Initiator Group ID Create Extent → point at zvol Create Target → link Portal Group ID + Initiator Group ID Associate Extent to Target at LUN 0 Never use the TrueNAS iSCSI wizard. It produces broken configurations. Always build manually.\nGetting started with your own homelab # You don\u0026rsquo;t need enterprise hardware. The concepts work on anything.\nMinimum setup: one machine running Proxmox (works on most x86 hardware with virtualisation enabled in BIOS). Or Ubuntu Server with Docker if you want to skip the hypervisor layer.\nInstall Proxmox: download the ISO from proxmox.com, flash to USB, boot and install. It takes about 10 minutes.\nFirst LXC: Proxmox has a built-in template library. Download an Ubuntu 22.04 template and create an LXC from it in the web UI — takes 2 minutes. You get a full Ubuntu environment in ~50MB RAM.\nThe best approach: build up one layer at a time. One service teaches one concept. Proxmox teaches virtualisation. Adding Tailscale teaches WireGuard and kernel networking. Adding NPM teaches reverse proxies and TLS. Each problem you debug teaches something that transfers to the next service.\n","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/foundation/","section":"Projects","summary":"The concepts every other homelab page builds on — hypervisors, VMs vs LXC containers, ZFS storage, subnets and NAT, and iSCSI block devices. Read this first.","title":"Foundation","type":"projects"},{"content":"A self-hosted infrastructure lab on two Dell enterprise rack servers — the same hardware used in production data centres. The goal isn\u0026rsquo;t cost savings, it\u0026rsquo;s understanding. Every layer of the stack is visible, configurable, and breakable. When something breaks, you fix it yourself.\nEach page below is a standalone tutorial. Start with Foundation if you\u0026rsquo;re new — it covers the concepts every other page assumes. Service pages are independent; read whichever is relevant.\nHardware # Machine Role OS IP Dell R630 Compute Proxmox VE 9.1.1 192.168.5.146 Dell R730 Storage TrueNAS Scale 192.168.5.142 Network: 192.168.5.0/24, gateway 192.168.5.1. Public domain imadinc.com via Cloudflare with wildcard A record *.imadinc.com → 82.4.94.229.\nArchitecture # Internet │ port 80/443 forwarded through double NAT ▼ Linksys Router (192.168.5.1) │ ├── R630 Proxmox (192.168.5.146) │ ├── LXC 102: Tailscale (192.168.5.128) ← remote access │ ├── LXC 103: Nextcloud (192.168.5.134) ← self-hosted cloud │ ├── LXC 104: NPM (192.168.5.109) ← reverse proxy │ └── VM 101: arr stack (192.168.5.127) ← media server │ └── R730 TrueNAS (192.168.5.142) └── ZFS pool → datasets (NFS) + zvols (iSCSI) External: browser → Cloudflare → NPM → correct service Internal: Tailscale → subnet router LXC → any LAN device Services # ","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/","section":"Projects","summary":"Two Dell enterprise servers running a full self-hosted stack — Proxmox hypervisor, TrueNAS ZFS storage, iSCSI block devices, Tailscale mesh VPN, Nginx Proxy Manager, Nextcloud, and an automated media server.","title":"Homelab","type":"projects"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/in-progress/","section":"Tags","summary":"","title":"In-Progress","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/macos/","section":"Tags","summary":"","title":"Macos","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/malware-analysis/","section":"Tags","summary":"","title":"Malware-Analysis","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/networking/","section":"Tags","summary":"","title":"Networking","type":"tags"},{"content":"LXC 103 — IP: 192.168.5.134 — public at https://nextcloud.imadinc.com\nNextcloud is a self-hosted replacement for Google Drive, iCloud, and Dropbox. Files live on hardware you own, synced to all your devices via WebDAV. This page covers the full setup — what Nextcloud is, why the data directory is on iSCSI storage, and how to fix the reverse proxy issues that trip up every NextcloudPi deployment.\nWhat Nextcloud is # Nextcloud is software that runs on your own server and acts as a personal cloud. It provides:\nFile sync across all devices (PC, MacBook, iPhone) — drag files into the Nextcloud folder, they appear everywhere Web interface — browser access from anywhere WebDAV — a file protocol that apps can talk to directly (this is how Obsidian vault sync works) Photo backup from mobile Extensible — hundreds of apps (calendar, contacts, notes, collaborative editing) The trade-off vs Google Drive: you manage the infrastructure, backups, and uptime. In exchange, your files never leave your control and you\u0026rsquo;re not paying per GB.\nNextcloudPi vs plain Nextcloud # Plain Nextcloud: install Apache, PHP, MariaDB, Redis separately, configure permissions, wire them together, set up SSL. Many steps, many things that can go wrong.\nNextcloudPi: a pre-packaged distribution with Apache, PHP, MariaDB, Redis, and sensible defaults already configured. Install via a single community script. Includes a web management panel (ncp-web on port 4443) for point-and-click administration.\nThe trade-off: NextcloudPi assumes it\u0026rsquo;s the public-facing server handling SSL directly. Putting it behind a reverse proxy means Apache makes wrong assumptions about the connection — and you have to fix them. Every fix is documented in the NPM page and below.\nFor future rebuilds: plain Nextcloud (not NextcloudPi) behind a reverse proxy gives a cleaner architecture. More initial setup work, but no fighting Apache\u0026rsquo;s defaults.\nArchitecture # User device (WebDAV / browser) ↓ https://nextcloud.imadinc.com ↓ Cloudflare → NPM LXC 104 ↓ HTTP (NPM terminates HTTPS) Nextcloud LXC 103 (192.168.5.134) ↓ PHP reads/writes data directory /mnt/ncdata/ncdata/data ↓ iSCSI block device TrueNAS nextcloud-zvol (2TB sparse, formatted ext4) ↓ MyMassStorage ZFS pool MariaDB and Redis stay on LXC local disk — database queries are fast random I/O. Adding network latency via iSCSI for the database is unnecessary. Only the user data directory (the actual files) goes on TrueNAS.\nWhy iSCSI for the data directory — not NFS, not local disk # Three reasons iSCSI is the only correct choice here:\n1. NextcloudPi explicitly rejects NFS. The nc-datadir script that moves the data directory runs stat -fc%T on the target path and checks the filesystem type. It only accepts ext2, ext3, ext4, btrfs, or zfs. NFS returns nfs — rejected with an error. This is not configurable.\n2. File locking. Nextcloud writes thumbnails, cache, and upload chunks concurrently. File locking over NFS is historically unreliable — it depends on the NFS server\u0026rsquo;s lock daemon and can fail silently. iSCSI presents a raw block device formatted as ext4 — proper POSIX file locking, same as a local disk.\n3. Storage capacity. User files (photos, videos, documents) need terabytes. The LXC\u0026rsquo;s local disk is 8GB — OS and binaries only. TrueNAS has the capacity, and the iSCSI zvol is sparse (thin-provisioned): it\u0026rsquo;s configured at 2TB but only consumes actual data space.\nSetup # 1. Create the LXC # # On Proxmox host — Nextcloud community script bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/nextcloudpi.sh)\u0026#34; Assign IP 192.168.5.134 as static in the LXC config.\n2. Create and attach the iSCSI zvol # On TrueNAS:\nStorage → Datasets → Add Zvol under MyMassStorage Name: nextcloud-zvol, Size: 2TiB, Sparse: enabled Configure iSCSI (Portal → Initiator Group with Proxmox IQN → Extent → Target → LUN) — see Foundation for the full iSCSI setup On Proxmox host (after iSCSI target is available):\n# Discover target iscsiadm -m discovery -t sendtargets -p 192.168.5.142:3260 # Login iscsiadm -m node --targetname \u0026#34;iqn.2005-10.org.freenas.ctl:nextcloud-target\u0026#34; \\ --portal 192.168.5.142:3260 --login # Check disk appeared (look for new sdX) lsblk # Format (first time only) mkfs.ext4 /dev/sdX # Get persistent path ls -la /dev/disk/by-path/ | grep sdX # Add to LXC config (/etc/pve/lxc/103.conf) # mp0: /dev/disk/by-path/ip-192.168.5.142:3260-iscsi-...,mp=/mnt/ncdata,backup=0 3. Move Nextcloud data directory to the iSCSI mount # In the ncp-web panel (port 4443, Tailscale only) → nc-datadir → point to /mnt/ncdata. The panel may show a warning even when it succeeds — verify with df -h /mnt/ncdata.\n4. Fix the reverse proxy issues # See the NPM page for the full Apache redirect fix and occ commands. Summary:\n# Fix 1: use real domain in redirect pct exec 103 -- sed -i \\ \u0026#39;s|https://%{SERVER_NAME}/$1|https://nextcloud.imadinc.com/$1|\u0026#39; \\ /etc/apache2/sites-enabled/000-default.conf # Fix 2: don\u0026#39;t redirect if request claims HTTPS via X-Forwarded-Proto pct exec 103 -- sed -i \\ \u0026#39;s|RewriteCond %{HTTPS} !=on|RewriteCond %{HTTPS} !=on\\n RewriteCond %{HTTP:X-Forwarded-Proto} !https|\u0026#39; \\ /etc/apache2/sites-enabled/000-default.conf # Tell Nextcloud it\u0026#39;s behind a proxy pct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set trusted_proxies 0 --value=\u0026#34;192.168.5.109\u0026#34; pct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set overwriteprotocol --value=\u0026#34;https\u0026#34; pct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set overwrite.cli.url --value=\u0026#34;https://nextcloud.imadinc.com\u0026#34; 5. Create your user account # In the Nextcloud web UI, create a personal user account (separate from the ncp admin account). Don\u0026rsquo;t use the ncp account for daily use.\nUsing it: Obsidian vault sync # Nextcloud provides WebDAV access — a file protocol that Obsidian can use directly as a sync provider.\nObsidian Remotely Save plugin:\nInstall the \u0026ldquo;Remotely Save\u0026rdquo; community plugin Service: WebDAV Address: https://nextcloud.imadinc.com/remote.php/dav/files/yourusername/ObsidianVault Username and password: your Nextcloud user credentials The vault syncs to all devices with this plugin installed — desktop, mobile, everything. Files live in your Nextcloud data directory on the 2TB iSCSI zvol on TrueNAS.\nZFS snapshot backup: TrueNAS automatically snapshots the Nextcloud zvol on a schedule. Every version of every file is recoverable.\nVerification commands # # Confirm data directory is on iSCSI (should show 2TB and /dev/sdX) pct exec 103 -- df -h /mnt/ncdata/ncdata/data # Check key Nextcloud config values pct exec 103 -- grep -E \u0026#34;datadirectory|overwrite|trusted\u0026#34; \\ /var/www/nextcloud/config/config.php # Verify redirect resolves to correct domain (not LXC IP) pct exec 103 -- curl -v http://192.168.5.134 2\u0026gt;\u0026amp;1 | grep Location # Expected: Location: https://nextcloud.imadinc.com/ # Check services pct exec 103 -- systemctl status apache2 pct exec 103 -- systemctl status mariadb Key lessons # ncp-web admin panel (port 4443) is Tailscale-only — never exposed publicly Use occ to change Nextcloud config, never edit config.php directly NextcloudPi\u0026rsquo;s \u0026ldquo;nc-datadir path doesn\u0026rsquo;t exist\u0026rdquo; warning can be a UI glitch — the script may have succeeded even if the warning shows; verify with df -h If rebuilding from scratch, consider plain Nextcloud (not NextcloudPi) — more setup, cleaner architecture, no Apache redirect fights ","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/nextcloud/","section":"Projects","summary":"Self-hosted file sync and cloud storage on a Nextcloud LXC, with data stored on a TrueNAS iSCSI zvol. Covers why iSCSI (not NFS), how to fix NextcloudPi behind a reverse proxy, and how to use it for Obsidian vault sync.","title":"Nextcloud — Self-Hosted Cloud","type":"projects"},{"content":"LXC 104 — IP: 192.168.5.109 — admin panel at :81 (Tailscale only)\nThis page covers how all public traffic reaches the homelab — and how each domain name gets routed to the right service. The mechanism is a reverse proxy. Every concept is explained from scratch.\nThe problem: one port, many services # The homelab has multiple services running on different internal ports:\nNextcloud on 192.168.5.134:80 Plex on 192.168.5.127:32400 n8n on 192.168.5.x:5678 The internet only knows one address: the home public IP (82.4.94.229). And only one service can own port 443 (HTTPS) at a time.\nWithout a solution, you\u0026rsquo;d either:\nForward a different port for each service (ugly URLs, security exposure, port management nightmare) Or only have one public service The solution is a reverse proxy.\nWhat a reverse proxy does # A reverse proxy is a server that sits in front of all your services, listening on port 443. It reads the domain name of each incoming request and routes it to the right backend:\nnextcloud.imadinc.com → 192.168.5.134:80 plex.imadinc.com → 192.168.5.127:32400 n8n.imadinc.com → 192.168.5.x:5678 The client (browser, app, WebDAV sync) only ever talks to the proxy on port 443. It never sees internal IPs or ports. Clean URLs, single entry point.\nThe \u0026ldquo;reverse\u0026rdquo; in reverse proxy: a regular (forward) proxy sits in front of clients — it makes outgoing requests on their behalf (e.g. a corporate proxy that all employee traffic goes through). A reverse proxy sits in front of servers — it receives incoming requests on their behalf and routes them internally. Same mechanism, opposite direction.\nSSL termination # The reverse proxy handles HTTPS for every service. This is called SSL termination — the proxy terminates the encrypted connection, decrypts the traffic, and forwards plain HTTP to the backend:\nBrowser → Cloudflare (HTTPS) → NPM (HTTPS, Let\u0026#39;s Encrypt cert) → Service (HTTP) Two separate TLS connections happen:\nBrowser ↔ Cloudflare: Cloudflare\u0026rsquo;s edge certificate Cloudflare ↔ NPM: Let\u0026rsquo;s Encrypt certificate that NPM manages NPM → backend services is plain HTTP on the trusted internal LAN. Backend services don\u0026rsquo;t need HTTPS configured at all — the proxy handles it.\nCloudflare SSL mode: Full. This means Cloudflare verifies NPM has a certificate (but doesn\u0026rsquo;t require it to be CA-signed). End-to-end encryption. If set to \u0026ldquo;Flexible\u0026rdquo;, Cloudflare would accept HTTP from NPM — that would mean unencrypted traffic on the path from Cloudflare to home.\nLet\u0026rsquo;s Encrypt: why DNS challenge is required # Let\u0026rsquo;s Encrypt issues free SSL certificates but must verify you control the domain.\nStandard HTTP challenge: Let\u0026rsquo;s Encrypt visits http://nextcloud.imadinc.com/.well-known/acme-challenge/\u0026lt;token\u0026gt;. NPM creates the challenge file, Let\u0026rsquo;s Encrypt reads it, ownership confirmed.\nWhy this fails here: Cloudflare is proxying traffic for imadinc.com. When Let\u0026rsquo;s Encrypt visits the HTTP challenge URL, Cloudflare intercepts the request before it reaches NPM. Let\u0026rsquo;s Encrypt is talking to Cloudflare\u0026rsquo;s edge, not to NPM. The challenge file never gets read from NPM.\nDNS challenge: completely different mechanism. Let\u0026rsquo;s Encrypt asks you to prove ownership by creating a specific TXT record in your DNS zone:\n_acme-challenge.nextcloud.imadinc.com → \u0026lt;random-token\u0026gt; Let\u0026rsquo;s Encrypt queries DNS directly (this bypasses the Cloudflare proxy entirely — it\u0026rsquo;s a DNS lookup, not an HTTP request). It sees the TXT record, confirms ownership, issues the certificate.\nNPM automates this using the Cloudflare API. When a certificate needs to be issued or renewed, NPM uses the API token to create the TXT record, waits for Let\u0026rsquo;s Encrypt to verify, then deletes the TXT record. All automatic. Certificates renew every 90 days without any manual action.\nCloudflare API token # The token needs exactly these permissions:\nZone → DNS → Edit — to create and delete TXT records Scoped to imadinc.com only — if the token is ever compromised, the attacker can only modify DNS records for this one domain, not touch the Cloudflare account Create it at: Cloudflare dashboard → My Profile → API Tokens → Create Token → \u0026ldquo;Edit zone DNS\u0026rdquo; template.\nNginx Proxy Manager # NPM is a web UI built on Nginx. Nginx is a high-performance web server and reverse proxy. NPM makes its configuration manageable through a browser UI — no hand-editing of Nginx config files.\nThe admin panel runs on port 81 (Tailscale-only — never publicly exposed).\nAdding a new service # For every new service deployed on the homelab:\nNPM → Proxy Hosts → Add: enter the domain (servicename.imadinc.com), forward to internal IP and port SSL tab: select the wildcard Let\u0026rsquo;s Encrypt certificate (already issued for *.imadinc.com) or request a new one No DNS changes needed — the wildcard A record *.imadinc.com → 82.4.94.229 already covers any new subdomain No router changes needed — ports 80 and 443 are already forwarded to NPM Three steps, new service is live with HTTPS.\nThe Nextcloud redirect loop problem # This section documents a real debugging problem that took time to trace and is a good example of how reverse proxy assumptions can break things.\nNextcloudPi (the preconfigured Nextcloud distribution) was designed to be the public-facing server handling its own HTTPS. Behind NPM, its Apache configuration makes broken assumptions.\nWhat happens: NPM sends plain HTTP to Nextcloud (http://192.168.5.134). Apache inside Nextcloud sees HTTP — not HTTPS — and fires its HTTPS redirect:\nLocation: https://{SERVER_NAME}/ where {SERVER_NAME} is the LXC\u0026rsquo;s IP (192.168.5.134), not the public domain. NPM receives Location: https://192.168.5.134/, tries to follow it, gets redirected again — ERR_TOO_MANY_REDIRECTS.\nTwo Apache fixes:\nFix 1 — make the redirect use the real domain, not the server IP:\npct exec 103 -- sed -i \\ \u0026#39;s|https://%{SERVER_NAME}/$1|https://nextcloud.imadinc.com/$1|\u0026#39; \\ /etc/apache2/sites-enabled/000-default.conf Fix 2 — don\u0026rsquo;t redirect at all if the request came from a proxy claiming HTTPS via X-Forwarded-Proto header:\npct exec 103 -- sed -i \\ \u0026#39;s|RewriteCond %{HTTPS} !=on|RewriteCond %{HTTPS} !=on\\n RewriteCond %{HTTP:X-Forwarded-Proto} !https|\u0026#39; \\ /etc/apache2/sites-enabled/000-default.conf Nextcloud also needs to know: it\u0026rsquo;s behind a proxy. Without this, Nextcloud generates internal URLs using the LXC IP instead of the public domain:\npct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set trusted_proxies 0 --value=\u0026#34;192.168.5.109\u0026#34; pct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set overwriteprotocol --value=\u0026#34;https\u0026#34; pct exec 103 -- sudo -u www-data php /var/www/nextcloud/occ \\ config:system:set overwrite.cli.url --value=\u0026#34;https://nextcloud.imadinc.com\u0026#34; occ is Nextcloud\u0026rsquo;s admin CLI. Use it instead of editing config.php directly — it validates input and runs as the correct user.\nTroubleshooting # Symptom Cause Fix Cloudflare 522 NPM unreachable Check port forwarding on both routers; check NPM is running SSL handshake failed Certificate not issued or wrong Cloudflare mode Check Cloudflare SSL mode is Full; check NPM cert ERR_TOO_MANY_REDIRECTS Backend forcing HTTPS + NPM also forcing Apply the two Apache fixes above Certificate issuance fails API token wrong scope or expired Recreate token: Zone → DNS → Edit, scoped to imadinc.com NPM admin login broken Stale SQLite database sqlite3 /data/database.sqlite \u0026quot;DELETE FROM user; DELETE FROM auth;\u0026quot; then stop+start (not restart) 504 gateway timeout Backend service unreachable Check the backend LXC/VM is running; check the IP and port in NPM config ","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/npm/","section":"Projects","summary":"How to route all public traffic through one entry point using Nginx Proxy Manager — reverse proxy fundamentals, SSL termination, Let’s Encrypt with DNS challenge, and the Cloudflare API integration.","title":"Nginx Proxy Manager — Public Access","type":"projects"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/proxmox/","section":"Tags","summary":"","title":"Proxmox","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/reverse-engineering/","section":"Tags","summary":"","title":"Reverse-Engineering","type":"tags"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/self-hosted/","section":"Tags","summary":"","title":"Self-Hosted","type":"tags"},{"content":"LXC 102 — IP: 192.168.5.128\nThis page covers remote access to the homelab from anywhere — over mobile data, from a university network, from behind any NAT. The mechanism is Tailscale, which builds a mesh VPN on top of WireGuard. Every concept is explained from scratch.\nThe problem: getting home through NAT # The homelab devices have private IPs (192.168.5.x). Private IPs are not routable on the internet — there\u0026rsquo;s no way to directly connect to 192.168.5.146 from a laptop on mobile data.\nThe naive fix — port forwarding — works for specific ports and specific services. But you\u0026rsquo;d need to forward a port for every service, expose them all to the internet, and manage credentials for each. That\u0026rsquo;s not remote access to the infrastructure — that\u0026rsquo;s individual service exposure.\nWhat\u0026rsquo;s needed: a way to make the homelab appear as if it\u0026rsquo;s on the same local network as the remote device, regardless of where that device is. That\u0026rsquo;s a VPN.\nWireGuard: how a VPN tunnel actually works # Tailscale is built on WireGuard, so understanding WireGuard first makes Tailscale much clearer.\nA VPN creates an encrypted tunnel between two devices. All traffic through the tunnel looks like normal traffic to anyone observing the network — they see encrypted packets between two endpoints, nothing more.\nHow WireGuard does this:\nEvery WireGuard peer has a key pair — a private key that never leaves the device, and a public key that you share openly. The private key signs outgoing packets. The public key lets others verify those signatures. When two peers exchange public keys and configure each other, they can establish a direct encrypted tunnel.\n# Server config (running at home) [Interface] Address = 10.0.0.1/24 # VPN address for this peer ListenPort = 51820 PrivateKey = \u0026lt;server-private-key\u0026gt; [Peer] PublicKey = \u0026lt;laptop-public-key\u0026gt; AllowedIPs = 10.0.0.2/32 # traffic from this IP is from this peer # Client config (laptop, anywhere) [Interface] Address = 10.0.0.2/24 PrivateKey = \u0026lt;laptop-private-key\u0026gt; [Peer] PublicKey = \u0026lt;server-public-key\u0026gt; Endpoint = 82.4.94.229:51820 # home public IP AllowedIPs = 192.168.5.0/24 # route home network traffic through tunnel The AllowedIPs on the client says: \u0026ldquo;any packet destined for 192.168.5.x should be encrypted and sent through the tunnel to this peer.\u0026rdquo; The home server decrypts it and forwards it on the LAN as a normal packet.\nThe manual WireGuard problem: you have to manually exchange public keys with every device. Every new device means updating configs everywhere. For a personal network with many devices this becomes unmanageable fast.\nTailscale: WireGuard without the key management # Tailscale handles the key exchange automatically. Each device installs the Tailscale client, authenticates once (via Google/GitHub/email), and gets a stable 100.x.x.x IP from the RFC 6598 CGNAT range — reserved for exactly this kind of use.\nBehind the scenes, Tailscale runs a coordination server. It doesn\u0026rsquo;t relay traffic — it just introduces devices. When two Tailscale devices want to communicate:\nBoth connect to the coordination server and share their current external IP/port The coordinator shares this info between the two devices The devices try to establish a direct WireGuard tunnel using NAT traversal NAT traversal works by having both devices send packets to each other simultaneously. Most home NAT routers, when they see an outgoing packet, create a temporary \u0026ldquo;hole\u0026rdquo; in the NAT table for replies. If both devices open holes at the same time, packets from each side arrive before the holes close — a direct tunnel forms without any port forwarding.\nThis works through double NAT (Virgin Media hub → Linksys → LXC), carrier-grade NAT, university firewalls, and most corporate firewalls. If a direct tunnel can\u0026rsquo;t form, Tailscale falls back to relaying through its DERP servers — but direct tunnels form in the vast majority of cases.\nSubnet routing: one device, entire LAN # Installing Tailscale on every homelab device isn\u0026rsquo;t practical — TrueNAS, every LXC, the Proxmox host itself. Instead, one device acts as a subnet router: it advertises the entire 192.168.5.0/24 subnet to the tailnet.\nLXC 102 is configured to tell Tailscale: \u0026ldquo;I can reach everything on 192.168.5.0/24. Route traffic for that subnet through me.\u0026rdquo;\nResult: any Tailscale device anywhere can reach 192.168.5.146 (Proxmox), 192.168.5.142 (TrueNAS), 192.168.5.134 (Nextcloud), etc. — at their normal LAN IPs, without Tailscale installed on any of them.\nThe reply traffic problem # There\u0026rsquo;s a subtle networking issue that breaks subnet routing until you fix it.\nWhen a Tailscale client (e.g. a laptop at 100.64.x.x) reaches 192.168.5.142 (TrueNAS), the packet flows correctly:\nLaptop → Tailscale tunnel → LXC 102 → 192.168.5.0/24 → TrueNAS But TrueNAS\u0026rsquo;s reply goes to its gateway — the Linksys router at 192.168.5.1. The Linksys receives a packet destined for 100.64.x.x and has no idea where that is. It drops it.\nFix: add a static route to the Linksys:\nDestination: 100.64.0.0, Subnet mask: 255.255.0.0 Gateway: 192.168.5.128 (the Tailscale LXC) Now every LAN device knows: \u0026ldquo;traffic destined for any 100.x.x.x address goes to the Tailscale LXC, which handles it.\u0026rdquo;\nWhy a dedicated LXC (not Docker, not the Proxmox host) # Not on Proxmox host: The hypervisor must stay stable. A Tailscale misconfiguration or failed update breaking networking could take down every VM and LXC simultaneously. That\u0026rsquo;s a hard lock-out.\nNot Docker: Tailscale needs to create TUN virtual network interfaces and manipulate kernel routing tables. Docker is designed for application isolation, the opposite of network control. Getting Docker to do subnet routing requires punching holes through the isolation that makes Docker useful in the first place.\nLXC chosen: ~50MB RAM overhead, full Linux environment, kernel capabilities can be granted selectively, completely isolated from other services. If Tailscale breaks, pct stop 102 from the Proxmox console fixes it.\nSetup # 1. Create the LXC # Use the Proxmox community script (Ubuntu-based, pre-installs Tailscale):\n# Run on Proxmox host bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/tailscale.sh)\u0026#34; 2. Enable TUN device access # Unprivileged LXCs don\u0026rsquo;t have access to /dev/net/tun (the virtual network interface WireGuard needs) by default. Add to /etc/pve/lxc/102.conf on the Proxmox host:\nlxc.cgroup2.devices.allow = c 10:200 rwm lxc.mount.entry = /dev/net/tun dev/net/tun none bind,create=file The c 10:200 is the character device major:minor for /dev/net/tun. rwm = read, write, mknod.\nRestart the LXC after editing: pct stop 102 \u0026amp;\u0026amp; pct start 102\n3. Enable IP forwarding inside the LXC # # Inside LXC 102 echo \u0026#39;net.ipv4.ip_forward = 1\u0026#39; | tee -a /etc/sysctl.conf sysctl -p Without IP forwarding, the kernel silently drops packets that arrive on one network interface destined for a different network. Subnet routing requires forwarding packets from the WireGuard tunnel to eth0 and vice versa.\n4. Connect to Tailscale and advertise the subnet # tailscale up --advertise-routes=192.168.5.0/24 --accept-routes This prints an auth URL. Open it in a browser, authenticate, and the LXC joins the tailnet.\n5. Approve the subnet route # In the Tailscale admin panel (login.tailscale.com/admin), find LXC 102 and approve the advertised subnet route. Tailscale doesn\u0026rsquo;t auto-approve subnet routes — this is a deliberate security gate.\n6. Fix UDP GRO (WireGuard performance) # Generic Receive Offload (GRO) batches UDP packets before handing them to the kernel. This improves performance for most UDP traffic but interferes with WireGuard\u0026rsquo;s packet processing:\napt install ethtool -y ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off Persist via a systemd service so it survives reboots — see Tailscale\u0026rsquo;s Linux docs for the service file.\n7. Static route on the Linksys # In the Linksys router admin UI under Advanced Routing:\nDestination IP: 100.64.0.0 Subnet mask: 255.255.0.0 Gateway: 192.168.5.128 Security boundary # Tailscale creates a clear security perimeter. Two categories of services:\nTailscale-only (never public):\nProxmox web UI (192.168.5.146:8006) TrueNAS web UI (192.168.5.142) NPM admin panel (192.168.5.109:81) All planned AI pipeline services (n8n, Chroma, Claude Code, Obsidian MCP) Public via NPM (Cloudflare → NPM → service):\nnextcloud.imadinc.com Future: plex.imadinc.com Anything not exposed via NPM is invisible from the internet. The only public entry points are port 80 and 443, both going to NPM. Everything sensitive requires a Tailscale connection — authenticated, encrypted, zero-trust.\nThis is the homelab equivalent of Zero Trust Network Access (ZTNA) — the same model used by Cloudflare Access, Zscaler, and similar enterprise products.\nTroubleshooting # # Check IP forwarding cat /proc/sys/net/ipv4/ip_forward # must return 1 # Check TUN device exists inside LXC ls /dev/net/tun # Check Tailscale service systemctl status tailscaled # Check subnet is being advertised tailscale status # Check UDP GRO state ethtool -k eth0 | grep -E \u0026#34;gro|rx-udp\u0026#34; Tailscale connects but can\u0026rsquo;t reach LAN devices: check subnet route is approved in admin panel, check IP forwarding is on, check static route on Linksys.\nLAN devices can reply sometimes but not consistently: static route missing or pointing at wrong gateway.\nTUN device not found: LXC config lines weren\u0026rsquo;t applied, or LXC wasn\u0026rsquo;t restarted after adding them.\n","date":"7 June 2026","externalUrl":null,"permalink":"/projects/homelab/tailscale/","section":"Projects","summary":"How to access every homelab device from anywhere using Tailscale mesh VPN — WireGuard fundamentals, NAT traversal, subnet routing, and the static route fix that makes LAN replies work.","title":"Tailscale — Remote Access","type":"projects"},{"content":"This page is my working notes through The Art of Mac Malware by Patrick Wardle — but before the chapter notes, a proper foundation: what malware analysis is, why macOS specifically, how the security model works, and how to get started yourself.\nWhat is malware? # Malware (malicious software) is any program that does something the user didn\u0026rsquo;t consent to, on behalf of someone else. That definition is intentionally broad, because malware covers a huge range of behaviour:\nRansomware — encrypts your files and demands payment for the key Spyware / RATs — records your screen, keystrokes, or camera and exfiltrates the data Adware — injects ads or redirects your browser to generate revenue Backdoors — open a persistent, hidden channel for remote access to your machine Infostealers — harvest credentials, cookies, and crypto wallets Rootkits — embed deep in the OS (sometimes the kernel) to hide their own presence A single piece of malware often combines multiple of these. A dropper downloads and installs other payloads. A loader decrypts and executes malware in memory without writing it to disk.\nWhat is malware analysis? # Malware analysis is the process of taking a piece of malicious software and figuring out exactly what it does — how it works, what systems it targets, what data it steals, and how to detect or stop it.\nThere are two fundamental approaches, and they complement each other.\nStatic analysis # Static analysis means examining the file without executing it. You\u0026rsquo;re reading the code and data as a document, not running it.\nWhat you look for:\nStrings — URLs, file paths, registry keys, error messages embedded in the binary Imports — which system functions the binary calls (tells you capabilities at a high level) File structure — how the binary is organised, whether it\u0026rsquo;s packed or obfuscated Code logic — disassembling machine code back to assembly or decompiling to pseudocode Tools for static analysis:\nstrings — extracts readable text from any file file — identifies file type from magic bytes, not extension otool -L — on macOS, lists shared libraries the binary links against nm — lists symbols (function/variable names) in a binary class-dump — recovers Objective-C class structure from a macOS binary Hopper / Ghidra / Binary Ninja — full disassemblers and decompilers Static analysis is safe — you\u0026rsquo;re just reading. But sophisticated malware is often packed or obfuscated: the real code is encrypted inside a loader that decrypts and runs it at runtime. Static analysis hits a wall at obfuscation; dynamic analysis continues past it.\nDynamic analysis # Dynamic analysis means actually running the malware in a controlled environment and observing what it does.\nWhat you observe:\nFile system activity — what files does it create, modify, or delete? Network traffic — what does it connect to? What data does it send? Process activity — what processes does it spawn? What does it inject into? System calls — every interaction with the OS kernel is a system call — logging these shows exactly what the malware is doing at the lowest level Tools for dynamic analysis on macOS:\nLLDB — the macOS debugger; attach to a running process, set breakpoints, inspect memory dtrace / fs_usage / opensnoop — trace file system and system call activity Wireshark / mitmproxy — capture and inspect network traffic Process Monitor (Objective-See) — GUI tool showing process activity in real time Netiquette (Objective-See) — shows active network connections per process Dynamic analysis requires isolation. You run malware in a virtual machine — a sandboxed environment where it can\u0026rsquo;t affect your real system. Snapshots let you restore to a clean state between runs. The malware executes thinking it\u0026rsquo;s on a real machine, and you watch everything it does.\nWhy macOS specifically? # For years, macOS had a reputation for being more secure than Windows. That reputation was partly earned and partly mythology — Mac users historically were fewer and less valuable targets, so attackers focused elsewhere. That\u0026rsquo;s changed.\nApple Silicon, growing enterprise Mac adoption, and macOS\u0026rsquo;s access to valuable credentials (iCloud, Apple ID, crypto wallets) have made it an increasingly attractive target. macOS-specific malware families now number in the hundreds.\nThere\u0026rsquo;s also a skills gap. Windows malware analysis is well-documented and well-tooled. macOS analysis has fewer practitioners, fewer tools, and less documentation. That scarcity makes it more valuable to understand.\nThe macOS security model # To understand how malware works on macOS, you have to understand what it\u0026rsquo;s working against — the defensive layers Apple has built in.\nGatekeeper # Gatekeeper checks every application before it first runs. It verifies that the app is signed with a valid Apple Developer certificate and, for apps downloaded from the internet, that it has been notarised — scanned by Apple\u0026rsquo;s automated system and approved.\nMalware bypasses this by:\nExploiting bugs in Gatekeeper itself Social engineering users into explicitly overriding it Abusing legitimate developer accounts to sign malicious code Targeting the period between download and first run System Integrity Protection (SIP) # SIP restricts what even root can do. Certain directories (/System, /usr, /bin, /sbin) and kernel-level operations are protected even from the superuser. You cannot modify these without booting into recovery mode and explicitly disabling SIP.\nSIP exists because malware historically escalated to root and then modified core system files to persist. SIP makes that category of persistence much harder.\nTransparency, Consent, and Control (TCC) # TCC is the permissions database you see when an app asks for access to your camera, microphone, contacts, or files. Apps must declare what they need and users must explicitly grant it.\nMalware targeting sensitive data must either get the user to grant permissions, exploit a TCC bypass vulnerability, or abuse an app that already has permissions.\nThe sandbox # Sandboxed apps (all Mac App Store apps, and many third-party apps) run in an isolated environment. They can only access files, network resources, and hardware capabilities explicitly declared in their entitlements. A sandboxed app cannot read another app\u0026rsquo;s data.\nNotarisation # Before distributing outside the App Store, developers submit their apps to Apple\u0026rsquo;s notarisation service. Apple\u0026rsquo;s systems scan for known malware and obvious bad behaviour. Apps that pass get a notarisation ticket embedded in them — Gatekeeper checks for this ticket.\nPersistence: how malware survives reboots # Most malware wants to survive reboots. The process of establishing a presence that persists across restarts is called persistence.\nmacOS has several legitimate mechanisms for launching software at startup — malware abuses all of them:\nLaunch Agents / Launch Daemons: Property list files (.plist) in specific directories that tell the OS to run a command at login or boot. The distinction: Launch Agents run in the user\u0026rsquo;s context (login), Launch Daemons run as root (boot).\n~/Library/LaunchAgents/ ← user-level, runs at login /Library/LaunchAgents/ ← all users, runs at login /Library/LaunchDaemons/ ← root, runs at boot A malicious LaunchAgent plist:\n\u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/Users/victim/.hidden/malware\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; Login Items: Applications added to System Settings → General → Login Items. Legitimate, visible, but abused.\nCron jobs: The Unix task scheduler. Malware can add cron entries to run at intervals.\nUnderstanding these persistence mechanisms is the foundation of both writing detections and understanding what a malware sample is doing when you see it create files in these locations.\nThe book: The Art of Mac Malware # Patrick Wardle is the foremost researcher in macOS security. He\u0026rsquo;s the founder of Objective-See (free, open-source macOS security tools), a former NSA analyst, and has discovered and presented dozens of macOS vulnerabilities and malware families.\nThe book covers:\nFile formats — Mach-O, the macOS executable format; how the kernel loads and runs programs Static analysis techniques — disassembly, decompilation, code analysis Dynamic analysis techniques — debugging, tracing, monitoring Persistence mechanisms — every way malware survives reboots on macOS Code injection and process manipulation — how malware injects into other processes Network-based malware — how command and control (C2) infrastructure works Rootkits and kernel extensions — the deepest, hardest-to-detect category Detection and defensive techniques — how to build tooling that catches these behaviours The book is freely available at taomm.org. There\u0026rsquo;s no reason not to read it.\nGetting started yourself # Install the free tools:\nObjective-See tools — free macOS security tools (Process Monitor, KnockKnock, Netiquette, and more) Ghidra — free, open-source reverse engineering tool from the NSA Hopper — macOS-native disassembler (paid, but better UX than Ghidra for Mac binaries) Set up a VM: Download UTM (free) or VMware Fusion (free for personal use). Create a macOS VM. Take a snapshot of the clean state before doing any analysis. Restore from the snapshot between runs.\nYour first sample: MalwareTraffic Analysis and VirusTotal are good sources for real samples once you\u0026rsquo;re set up in a VM. Start by running strings and file on a binary before executing anything.\n# Basic static analysis workflow file suspicious_binary # what is it? strings suspicious_binary # what readable content does it have? otool -L suspicious_binary # what libraries does it use? Notes and labs # Chapter writeups appear below as I work through the book. Each one covers the core concepts from the chapter, anything that surprised me, and practical lab exercises.\nIn progress — writeups added chapter by chapter.\n","date":"7 June 2026","externalUrl":null,"permalink":"/projects/taomm-notes/","section":"Projects","summary":"A tutorial introduction to macOS malware analysis — what malware is, how the macOS security model works, what static and dynamic analysis mean, and how to start analysing samples yourself using free tools.","title":"The Art of Mac Malware — Notes \u0026 Labs","type":"projects"},{"content":"","date":"7 June 2026","externalUrl":null,"permalink":"/tags/truenas/","section":"Tags","summary":"","title":"Truenas","type":"tags"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/tags/conferences/","section":"Tags","summary":"","title":"Conferences","type":"tags"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/categories/events/","section":"Categories","summary":"","title":"Events","type":"categories"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/tags/ios/","section":"Tags","summary":"","title":"Ios","type":"tags"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/tags/objective-see/","section":"Tags","summary":"","title":"Objective-See","type":"tags"},{"content":"","date":"1 August 2025","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"If you\u0026rsquo;re getting into macOS or iOS security, you\u0026rsquo;ll hear about OBTS eventually. Here\u0026rsquo;s what it actually is.\nThe short version # Objective by the Sea (#OBTS) is the world\u0026rsquo;s only conference dedicated entirely to Apple platform security — macOS, iOS, and related systems. It runs once a year, in a different location each time, and brings together the researchers, engineers, and analysts who actually work on this stuff.\nv8 was in Ibiza, Spain. v9 is in Hawaii.\nWho runs it # It\u0026rsquo;s run by the Objective-See Foundation, a non-profit founded by Patrick Wardle — ex-NSA, ex-Synack, and the person behind most of the free macOS security tools you\u0026rsquo;ll find if you look: LuLu, KnockKnock, BlockBlock, and others. He also wrote The Art of Mac Malware, which is the closest thing to a textbook for this field.\nThe foundation is genuinely non-profit. 100% of merchandise sales go to local charities, matched by the foundation. Tickets for students and local attendees are free.\nWhat happens there # Three days of training, three days of talks, a CTF, and social events.\nThe talks are technical and current — not vendor pitches, not \u0026ldquo;here\u0026rsquo;s our product.\u0026rdquo; At v8 in Ibiza the lineup included researchers from Google Project Zero, Jamf, iVerify, and independent researchers presenting things like:\nXNU kernel exploitation DPRK threat actor infrastructure on macOS iOS sandbox internals TCC bypass via Spotlight macOS C2 detection through statistical analysis Apple\u0026rsquo;s Private Cloud Compute authentication protocol All talks are live-streamed free on the Objective-See YouTube channel.\nObjective for the We (OFTW) # OBTS has a companion event called Objective for the We (#OFTW) — a smaller, free, invite-only event specifically for students and early-career people. Less intimidating entry point, same community.\nOFTW v3 was in London in July 2025. That\u0026rsquo;s where I started. If you\u0026rsquo;re a student interested in Apple security, it\u0026rsquo;s worth applying.\nWhy it matters # Most security conferences are broad. OBTS is narrow — Apple platforms only, deep technical content, researchers who do this as their actual job.\nIf Apple security is where you want to go, this is the community.\n","date":"1 August 2025","externalUrl":null,"permalink":"/posts/what-is-obts/","section":"Posts","summary":"The world’s only dedicated Apple security conference — what it is, who runs it, and why it exists.","title":"What is Objective by the Sea (OBTS)?","type":"posts"},{"content":"","date":"26 July 2025","externalUrl":null,"permalink":"/categories/career/","section":"Categories","summary":"","title":"Career","type":"categories"},{"content":"","date":"26 July 2025","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"Career","type":"tags"},{"content":"","date":"26 July 2025","externalUrl":null,"permalink":"/tags/cybersecurity/","section":"Tags","summary":"","title":"Cybersecurity","type":"tags"},{"content":"I took a voice note walking out of OFTW v3 in London. This is what it said.\nCompanies don\u0026rsquo;t hire on technical knowledge # This was the most repeated theme across the panel. The people hiring you — especially at the senior level — care more about whether you\u0026rsquo;re genuinely curious and driven than whether you can rattle off CVE numbers or pass a technical quiz.\nIf you can show real passion for a specific area of security, they will teach you the rest. The technical content is learnable. The drive and curiosity either exists or it doesn\u0026rsquo;t, and experienced people can tell within a conversation.\nThis was said by practitioners who work in Apple security, threat intelligence, and incident response — people doing the actual hiring at the companies you\u0026rsquo;d want to work at.\nSoft skills are underrated # Articulation. Communication. Being able to explain a technical concept to someone who isn\u0026rsquo;t technical. Showing up to things and being present in a community.\nThese carry more weight than most students expect. A candidate who can explain what they\u0026rsquo;re working on clearly and who visibly cares about the field is more valuable than someone who scores higher on a technical test but gives nothing in conversation.\nProof of interest beats proof of knowledge # Two things that came up repeatedly:\nAttending events and conferences consistently. Not just once. Showing up regularly signals sustained interest in a way that a CV line can\u0026rsquo;t replicate. Anyone can list \u0026ldquo;attended conference X\u0026rdquo; — but people who keep showing up are easy to distinguish from people who went once for the LinkedIn post.\nWriting publicly about things you actually care about. A blog, even a short one, where you\u0026rsquo;re clearly working through something real. The key word is actually — panellists were clear that writing about things you\u0026rsquo;re not interested in leads to burnout fast. Write about the stuff that genuinely interests you, even if it feels niche.\nReading deeply matters more than a degree # One of the panellists said something that stuck with me: two books deeply read and properly understood — where you\u0026rsquo;ve worked through the examples, understood the internals, maybe built something from it — carry more weight in their eyes than a university degree in the subject.\nFor Apple security specifically, The Art of Mac Malware by Patrick Wardle came up. It\u0026rsquo;s free online. It\u0026rsquo;s the closest thing to a proper curriculum for this space that exists.\nDon\u0026rsquo;t self-reject # Apply to things even when you feel underqualified. The worst outcome is a no — which is where you\u0026rsquo;d be anyway if you didn\u0026rsquo;t apply. Several people in that room, including the panellists, got into the field by applying to things they weren\u0026rsquo;t sure they deserved.\nI came out of that panel and immediately started this blog.\n","date":"26 July 2025","externalUrl":null,"permalink":"/posts/breaking-into-cybersecurity/","section":"Posts","summary":"Notes from a panel at Objective For The We v3 — what practitioners actually look for when hiring, and why most people are thinking about it wrong.","title":"What the OFTW Panel Taught Me About Breaking Into Cybersecurity","type":"posts"},{"content":"Last month I attended Objective For The We (OFTW) v3 in London — a two-day event run by the Objective-See Foundation, aimed at students and early-career people interested in Apple platform security.\nHow I ended up there # I didn\u0026rsquo;t find the event through a search. A speaker from Jamf who I\u0026rsquo;d connected with at a conference reposted something I\u0026rsquo;d shared on LinkedIn, and that led to an invite. It was a reminder that showing up publicly — even a small post — creates unexpected paths.\nWhat OFTW is # OFTW is the more accessible companion to Objective by the Sea (OBTS) — the world\u0026rsquo;s only dedicated macOS/iOS security conference. Where OBTS is a full multi-day conference with paid trainings and talks from senior researchers, OFTW is free, invite-only, and aimed specifically at students. It\u0026rsquo;s funded by the Objective-See Foundation and sponsored by companies like Kandji.\nThe format over two days:\nDay 1: Trainings (hands-on, small groups) + a surprise evening activity in London Day 2: Talks + a panel discussion + happy hour What I took from it # The technical content was genuinely deep — macOS internals, iOS security, malware analysis approaches I hadn\u0026rsquo;t seen covered in any university module.\nThe panel discussion stuck with me more than I expected. A recurring theme: companies hire on curiosity and drive, not technical knowledge. If you can demonstrate genuine passion for a specific area, they\u0026rsquo;ll teach you the rest. Soft skills — communication, showing up, being visible in a community — carry more weight than grades.\nPractical takeaways I wrote down immediately after:\nStart writing publicly about things you\u0026rsquo;re genuinely interested in — burnout comes from writing about things you don\u0026rsquo;t care about Read deeply in your area — two books properly understood carry more weight with practitioners than a degree Attending events consistently signals commitment in a way a CV line can\u0026rsquo;t replicate Meeting Patrick Wardle # Patrick Wardle — founder of Objective-See, creator of macOS security tools like LuLu, KnockKnock, and BlockBlock, and author of The Art of Mac Malware — was there and genuinely accessible. It\u0026rsquo;s not often you get to talk directly with the person who wrote the book on a subject you\u0026rsquo;re trying to learn.\nWhat\u0026rsquo;s next # OBTS v9 is coming up in Hawaii in November 2026. I\u0026rsquo;m applying for the student scholarship. OFTW v3 made it clear this is the community and the technical direction I want to keep moving in.\n","date":"25 July 2025","externalUrl":null,"permalink":"/posts/oftw-v3-london/","section":"Posts","summary":"My experience at OFTW v3 in London — the Apple security community’s free introductory event for students, run by the Objective-See Foundation.","title":"Attending Objective For The We (OFTW) v3 — London","type":"posts"},{"content":"","date":"1 June 2025","externalUrl":null,"permalink":"/categories/homelab/","section":"Categories","summary":"","title":"Homelab","type":"categories"},{"content":"","date":"1 June 2025","externalUrl":null,"permalink":"/tags/homelab/","section":"Tags","summary":"","title":"Homelab","type":"tags"},{"content":" The problem with just studying # Security courses teach you what attacks look like in diagrams. Networking modules explain subnets on whiteboards. Cryptography gets covered in lectures with no lab component.\nNone of that tells you what it actually feels like to misconfigure a firewall rule and lock yourself out. Or to watch a service silently fail at 2am and have to trace why. Or to realise your mental model of how DNS works was wrong in a specific way that only becomes obvious when you try to run your own resolver.\nThe homelab exists to find those gaps.\nWhat it is # A server running in my room, 24/7, hosting services I actually use. Not a cloud instance — physical hardware I can touch, break, and rebuild without paying per hour for the privilege of making mistakes.\nEverything runs in Docker containers managed through Portainer. This means I can spin up a new service in minutes, and when I inevitably break something, rolling back is straightforward.\nWhat\u0026rsquo;s running # Network layer:\nPi-hole — DNS-level ad and tracker blocking for everything on the network. More interesting than it sounds once you start seeing what your devices are actually talking to. WireGuard — VPN so I can access everything remotely. Setting this up properly taught me more about networking than any module I\u0026rsquo;ve taken. Infrastructure:\nnginx — reverse proxy handling routing and SSL termination for all services Portainer — container management UI Monitoring:\nUptime tracking across services with alerting when something goes down What I\u0026rsquo;ve actually learned # Every outage is a lesson. Some examples:\nMisconfigured nginx upstream caused 502s I couldn\u0026rsquo;t explain for an hour — turned out to be a container name mismatch that only appeared because of how Docker\u0026rsquo;s internal DNS works Pi-hole blocked a dependency my VPN client needed — taught me to check DNS logs before assuming the problem is somewhere else WireGuard key rotation went wrong in a way that locked me out remotely — learned to always test key changes locally first None of these failures are in any textbook. They\u0026rsquo;re the actual knowledge.\nWhy it matters for security work # At CrowdStrike, a lot of threat intelligence work involves understanding how attackers move through infrastructure — lateral movement, persistence mechanisms, C2 communication. Understanding infrastructure from the defender\u0026rsquo;s side, including all the ways it fails, makes that work more concrete.\nYou can\u0026rsquo;t really understand how something gets compromised if you\u0026rsquo;ve never built and broken it yourself.\nWhat\u0026rsquo;s next # Expanding the monitoring stack, adding intrusion detection, and documenting each service properly. Writeups on individual components coming to the Projects section.\n","date":"1 June 2025","externalUrl":null,"permalink":"/posts/starting-my-homelab/","section":"Posts","summary":"University teaches security theory. The homelab is where I find out if any of it is actually true.","title":"Why I Built a Homelab (And What's Running in It)","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]