<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-05-18T16:03:33+00:00</updated><id>/feed.xml</id><title type="html">The Mosburn Lab</title><subtitle>Building a business-grade home lab for AI and security development — infrastructure as code, self-hosted services, and the discipline to treat your home network like production.
</subtitle><author><name>Michael Osburn</name></author><entry><title type="html">SSO Across the Stack: Building a Unified Lab Identity Layer</title><link href="/2026/05/17/self-hosted-sso-lab-stack/" rel="alternate" type="text/html" title="SSO Across the Stack: Building a Unified Lab Identity Layer" /><published>2026-05-17T00:00:00+00:00</published><updated>2026-05-17T00:00:00+00:00</updated><id>/2026/05/17/self-hosted-sso-lab-stack</id><content type="html" xml:base="/2026/05/17/self-hosted-sso-lab-stack/"><![CDATA[<p>The previous post in this series covered deploying a self-hosted development stack — Redmine for issue tracking, a wiki, a Git forge, and a team chat platform. What it didn’t cover was authentication: each service had its own user database, its own login page, and its own password to manage.</p>

<p>That’s not a lab. That’s a collection of services that happen to run on the same machine.</p>

<p>The follow-up work was building a proper identity layer: Keycloak as the OIDC provider, every service authenticated through it, users managed in one place. This post covers what that process actually looked like — including the parts that didn’t work.</p>

<h2 id="why-keycloak">Why Keycloak</h2>

<p>The short answer is that Keycloak is what you’d run in production. Auth0, Okta, and Entra ID are all excellent — and all subscription-based. For a lab environment where the point is to learn the tools before you need them professionally, running the enterprise-grade open-source equivalent makes more sense than paying for managed identity or wiring up something half-functional.</p>

<p>Keycloak handles:</p>
<ul>
  <li>OIDC and SAML (the two protocols you’ll encounter in enterprise environments)</li>
  <li>Realm isolation — multiple tenants on one instance</li>
  <li>Fine-grained client scopes and protocol mappers</li>
  <li>MFA, brute force protection, and session management</li>
  <li>A user federation layer for LDAP and Active Directory integration</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">mosburn</code> realm on the lab Keycloak instance is the single source of truth for lab identity. One user account. One password. Works across every service in the stack.</p>

<h2 id="the-stack">The stack</h2>

<p>The lab runs as a Docker Compose stack with an nginx reverse proxy routing traffic by subdomain:</p>

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>URL</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Keycloak</td>
      <td>keycloak.mosburn.lab</td>
      <td>Identity provider</td>
    </tr>
    <tr>
      <td>Redmine</td>
      <td>redmine.mosburn.lab</td>
      <td>Issue and project tracking</td>
    </tr>
    <tr>
      <td>Wiki.js</td>
      <td>wiki.mosburn.lab</td>
      <td>Documentation and wiki</td>
    </tr>
    <tr>
      <td>Forgejo</td>
      <td>forgejo.mosburn.lab</td>
      <td>Git forge</td>
    </tr>
  </tbody>
</table>

<p>Each service runs with its own PostgreSQL database. The nginx config proxies <code class="language-plaintext highlighter-rouge">*.mosburn.lab</code> subdomains to the relevant containers, and each container has <code class="language-plaintext highlighter-rouge">extra_hosts</code> pointing <code class="language-plaintext highlighter-rouge">keycloak.mosburn.lab</code> to the host gateway — so backchannel token exchange calls route correctly through nginx rather than failing to resolve the hostname inside the Docker network.</p>

<p>That last part took longer than it should have. OIDC has two distinct communication patterns that get confused in a containerised setup: the browser-facing redirects (which need the public hostname) and the server-to-server token exchange (which needs a hostname the container can actually resolve). Getting both working simultaneously requires understanding which path each call takes.</p>

<h2 id="what-the-free-tier-actually-means-for-sso">What the “free tier” actually means for SSO</h2>

<p>The first lesson was expensive in time: many self-hosted applications have quietly moved SSO behind a paywall.</p>

<p><strong>Mattermost</strong> was the original choice for team chat. It’s widely deployed, has a good mobile app, and seemed like the obvious pick. The OpenID Connect option in System Console has a prominent upsell wall requiring Professional. The GitLab OAuth option — historically free — also now requires Professional. Mattermost as a free self-hosted platform no longer supports SSO in any meaningful sense.</p>

<p><strong>Docmost</strong> was the original wiki choice. Clean interface, good editor, PostgreSQL backend. Version 0.80.2 ships with OIDC code visible in the dist bundle under <code class="language-plaintext highlighter-rouge">ee/sso</code> — enterprise edition. The <code class="language-plaintext highlighter-rouge">OIDC_ENABLED</code> environment variable that appears in older documentation does nothing in current releases.</p>

<p>Docmost was replaced with <strong>Wiki.js</strong>, which has free OIDC via a Generic OpenID Connect strategy. Mattermost was not replaced with another self-hosted chat platform — and the reasoning is worth spelling out, because it applies more broadly.</p>

<p>The pattern here is worth noting for anyone else building a self-hosted stack: always verify SSO support against the current version before building your deployment around a service. Documentation tends to lag behind licensing changes by months.</p>

<h2 id="alerts-need-to-reach-you">Alerts need to reach you</h2>

<p>A self-hosted chat platform sounds good in theory. In practice, the primary value of a chat system in a lab context isn’t discussion — it’s automation output. Build failures, deployment events, monitoring alerts, cron job completions. The things that interrupt you, or should.</p>

<p>Self-hosted chat doesn’t deliver those notifications to your phone when you’re out riding. A pipeline that fails overnight doesn’t show up until you’re back at a desk. A monitoring alert at 2am goes nowhere. The Redmine plugin that posts to a webhook might as well be logging to /dev/null if the webhook endpoint isn’t running a client you have in your pocket.</p>

<p>Discord already handles this. Webhooks are first-class, the mobile app is reliable, and the notification model actually works. CI/CD integrations are a few lines of configuration. You get the same “build failed, retry?” message over lunch that you’d see in a professional Slack or Teams setup — without running infrastructure to achieve it.</p>

<p>The lab stack uses Discord for automation output via webhooks. The Redmine <code class="language-plaintext highlighter-rouge">redmine_messenger</code> plugin posts issue activity to a dedicated channel. Forgejo CI can post pipeline results the same way. No server to maintain, no database to back up, no container to debug when a MongoDB replica set election decides to happen at an inconvenient time.</p>

<h2 id="configuring-oidc-across-heterogeneous-services">Configuring OIDC across heterogeneous services</h2>

<p>Each service has a different approach to OIDC configuration, which is instructive.</p>

<p><strong>Redmine</strong> uses the <code class="language-plaintext highlighter-rouge">devopskube/redmine_openid_connect</code> plugin — a CAS-style patch rather than OmniAuth. Configuration is through the plugin admin UI, not <code class="language-plaintext highlighter-rouge">configuration.yml</code>. The discovery document is cached; clearing <code class="language-plaintext highlighter-rouge">tmp/cache/</code> is required when changing realm configuration. There was also a Ruby bug in the plugin where <code class="language-plaintext highlighter-rouge">dynamic_config_expiry</code> was passed as a String to <code class="language-plaintext highlighter-rouge">Rails.cache.fetch</code> which expects a Numeric — patched in the Dockerfile with a <code class="language-plaintext highlighter-rouge">sed</code> one-liner after clone.</p>

<p><strong>Forgejo</strong> has native OAuth2 support with an OpenID Connect provider type. Configuration is through the admin UI with the Keycloak discovery URL. The only complication was ensuring the discovery URL used the public hostname without the internal port — Keycloak’s <code class="language-plaintext highlighter-rouge">KC_HOSTNAME</code> needs to be set to the full URL (<code class="language-plaintext highlighter-rouge">http://keycloak.mosburn.lab</code>) rather than just the hostname, otherwise the issuer includes <code class="language-plaintext highlighter-rouge">:8080</code> and backchannel calls from containers fail.</p>

<p><strong>Wiki.js</strong> uses a Generic OpenID Connect strategy (not Generic OAuth2 — they’re different entries and using the wrong one wastes time). The callback URL is UUID-based, not derived from the strategy display name, so you have to check the Configuration Reference section to find the actual redirect URI to register in Keycloak. Setting the Site URL in the admin UI is required to get the correct protocol — Wiki.js will advertise HTTPS if it detects a forwarded-HTTPS header, regardless of what you set in environment variables.</p>

<h2 id="ansible-integration">Ansible integration</h2>

<p>The Docker Compose stack is the lab testing environment. The production deployments — Redmine, Forgejo, and the others — are managed by the Ansible roles in the <code class="language-plaintext highlighter-rouge">mosburn.*</code> namespace and deployed to dedicated VMs via <code class="language-plaintext highlighter-rouge">lab.yml</code>.</p>

<p>The compose stack mirrors the production architecture closely enough that configuration validated here translates directly. Client IDs, redirect URIs, and token endpoint URLs carry over. The main difference is the hostname scheme: <code class="language-plaintext highlighter-rouge">*.mosburn.lab</code> with subdomains points at the local nginx in the lab, and at real hostnames with TLS in production.</p>

<p>The Keycloak <code class="language-plaintext highlighter-rouge">mosburn</code> realm and its client configurations are the portable artifact. When a new service is validated in the lab stack, the same Keycloak client configuration works in production — different URL, same auth flow.</p>

<h2 id="whats-left">What’s left</h2>

<p>The other gap is Vaultwarden — and it’s staying a gap. The technical case is straightforward: <code class="language-plaintext highlighter-rouge">1password</code> ships as a managed package in <code class="language-plaintext highlighter-rouge">mosburn.common</code>, Vaultwarden would replace it, and the SSO infrastructure built here would handle authentication with no extra work. The implementation would be an afternoon.</p>

<p>The question is whether it should be on the list at all.</p>

<p>There’s a maintenance cost attached to every self-hosted service. Updates, database backups, the occasional broken container, the plugin that stops working after a version bump. Most of the time that cost is invisible — until it isn’t, and you’re debugging a Redmine cache issue on a Sunday instead of being somewhere else.</p>

<p>I stopped running Gentoo on my daily driver for the same reason. It’s a spectacular operating system and I learned more maintaining it than I did from any formal source. But at some point the ratio of time spent maintaining the environment to time spent doing things in the environment tips the wrong way. A weekend afternoon that could go toward a long ride or an afternoon buried in medieval history is worth more than another self-hosted service I’ll spend three hours debugging when the next major release drops a breaking change.</p>

<p>1Password is paid software. It works. It gets security updates without my involvement. For credentials — which are the one category of data where a self-hosting failure has real consequences — the maintenance-free option is the right call.</p>

<p>The lab exists to learn things that transfer to professional contexts and to run services that genuinely benefit from local control. A password manager that syncs across devices and has a mobile app that works in a car park is not in that category. Neither is a chat platform.</p>

<p>Some things are worth paying for. Identity is infrastructure. Knowing which parts of that infrastructure to outsource is part of the discipline.</p>]]></content><author><name>Michael Osburn</name></author><category term="infrastructure" /><category term="security" /><category term="keycloak" /><category term="sso" /><category term="oidc" /><category term="docker" /><category term="redmine" /><category term="forgejo" /><category term="wikijs" /><category term="discord" /><category term="ansible" /><summary type="html"><![CDATA[Single sign-on isn't just a convenience feature. It's a security control. Building a unified identity layer across a self-hosted lab stack is harder than it looks — and what the vendors don't tell you about "free" SSO costs more in time than it saves in money.]]></summary></entry><entry><title type="html">The Commute Calculator: What a Hybrid Offer Actually Costs</title><link href="/2026/05/11/commute-calculator/" rel="alternate" type="text/html" title="The Commute Calculator: What a Hybrid Offer Actually Costs" /><published>2026-05-11T00:00:00+00:00</published><updated>2026-05-11T00:00:00+00:00</updated><id>/2026/05/11/commute-calculator</id><content type="html" xml:base="/2026/05/11/commute-calculator/"><![CDATA[<p>My wife got a hybrid role offer. Good job, real company, worth taking seriously. And almost immediately the conversation turned into: okay, but how do we actually compare this to what she’s doing now?</p>

<p>She’s remote. The new role is three days a week in office. Everyone acts like that’s a minor detail. It’s not.</p>

<p>The instinct is to think about gas. $80 a month, whatever, you factor it in and move on. But that’s not the cost. That’s the part of the cost you can see, and it’s also the smallest part.</p>

<h2 id="the-number-youre-not-thinking-about">The number you’re not thinking about</h2>

<p>Here’s the thing about fuel costs: they feel real because you’re physically pumping gas. But run the math on 25 miles each way, three days a week, and you’re at about $950 a year. Yeah. Less than a grand.</p>

<p>The IRS mileage rate — $0.67 a mile, which folds in depreciation, maintenance, insurance, all of it — gets you closer to $4,500. That’s a more honest number. Still not the one that matters.</p>

<p>Your time matters.</p>

<p>At $110k, you’re making about $53 an hour. A 45-minute commute each way, three days a week, is 202 hours a year you’re not getting back. That’s $10,700. Not in gas. Not in wear on the car. In hours.</p>

<p>More than double the vehicle cost. Most people walk into a salary negotiation thinking about $950. They should be walking in thinking about $15,200.</p>

<h2 id="the-gross-up-problem">The gross-up problem</h2>

<p>Even $15,200 isn’t the right number to use at the table, because salary is pre-tax and commute costs come out of the other side.</p>

<p>You need to gross it up. Figure out how much pre-tax salary you’d actually need to earn to cover what the commute takes from you post-tax.</p>

<p>At a combined marginal rate around 28% — federal plus Colorado — that $15,200 in real post-tax cost turns into $21,100 in required pre-tax salary. That’s what the commute actually costs you in offer terms.</p>

<p>So the hybrid offer at $110k is equivalent to a remote job paying $88,900.</p>

<p>If the remote option is sitting there at $100k, the $110k hybrid isn’t $10k better. It’s $11,100 worse. You’d need them at $121k before you’re genuinely ahead.</p>

<p>That’s not a rounding error you shrug off. That’s a whole different answer to the question “is this offer worth it.”</p>

<h2 id="so-i-asked-claude-to-build-it">So I asked Claude to build it</h2>

<p>Doing this math by hand every time you tweak a variable is stupid. Change the days per week, adjust the salary, swap in a different commute distance — you’re redoing everything from scratch.</p>

<p>I described the problem to Claude. The inputs, the two modes (IRS rate versus actual fuel cost and MPG), the time cost calculation, 2025 federal tax brackets for the gross-up, hybrid days dialed in separately from total work weeks. It built a full-stack FastAPI backend and React frontend in a day.</p>

<p>This is the <a href="/2026/05/04/self-hosted-ai-stack/">self-hosted AI stack</a> doing actual work on an actual problem. Not a proof of concept. The calculator handles commute distance, days per week, salary, parking costs. Shows you the pay period breakdown — exactly how much more per paycheck you’d need. Has an e-bike ROI panel that tells you break-even in months if you can bike part of the route. Metric and imperial toggles.</p>

<p>We used it. That’s the whole point.</p>

<h2 id="what-the-numbers-said">What the numbers said</h2>

<p>Here’s what the calculator spits out for the scenario above — 25 miles each way, 3 days per week, $110k, 45-minute commute:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Annual cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Vehicle cost (IRS rate)</td>
      <td>$4,500</td>
    </tr>
    <tr>
      <td>Time cost</td>
      <td>$10,700</td>
    </tr>
    <tr>
      <td><strong>Total commute cost</strong></td>
      <td><strong>$15,200</strong></td>
    </tr>
    <tr>
      <td>Gross-up to pre-tax (28%)</td>
      <td>$21,100</td>
    </tr>
    <tr>
      <td><strong>Equivalent remote salary</strong></td>
      <td><strong>$88,900</strong></td>
    </tr>
  </tbody>
</table>

<p>The offer letter says $110k. The number you’re negotiating from is $88,900.</p>

<p>Your numbers will be different. Shorter commute, two days a week, higher salary — the calculator takes all of it. The point isn’t the specific output. It’s that you run it before you walk in, not after.</p>

<p>The calculator doesn’t decide anything. It just makes sure you’re not negotiating against a number you made up in your head.</p>

<p>The repo is at <a href="https://github.com/mosburn/commute-calculator">github.com/mosburn/commute-calculator</a>. Clone it, run <code class="language-plaintext highlighter-rouge">make install &amp;&amp; make run</code>, put in your actual situation. Do it before you negotiate.</p>]]></content><author><name>Michael Osburn</name></author><category term="tools" /><category term="ai" /><category term="salary-negotiation" /><category term="commute" /><category term="hybrid-work" /><category term="fastapi" /><category term="react" /><category term="claude" /><summary type="html"><![CDATA[Hybrid role offers look different once you account for the time cost of commuting and gross up the numbers to pre-tax. I asked Claude to build a calculator. Here's what it showed us.]]></summary></entry><entry><title type="html">Home SOC: Security Research with TheHive and Cortex</title><link href="/2026/05/06/home-soc-thehive-cortex/" rel="alternate" type="text/html" title="Home SOC: Security Research with TheHive and Cortex" /><published>2026-05-06T00:00:00+00:00</published><updated>2026-05-06T00:00:00+00:00</updated><id>/2026/05/06/home-soc-thehive-cortex</id><content type="html" xml:base="/2026/05/06/home-soc-thehive-cortex/"><![CDATA[<p>Security research is an unusual hobby. The tooling is powerful, the learning curve is steep, and the infrastructure requirements are substantial enough that most people doing it at home are either running underpowered setups or spinning up cloud instances they forget to turn off between sessions.</p>

<p>The Mosburn Lab takes a different approach: infrastructure-as-code deployment of professional security tooling, torn down and rebuilt when needed, with reproducible state managed entirely by Ansible.</p>

<h2 id="what-a-home-soc-actually-means">What a home SOC actually means</h2>

<p>A Security Operations Center, in enterprise terms, is a team with a toolchain for detecting, investigating, and responding to incidents. The core stack typically includes:</p>

<ul>
  <li><strong>SIEM</strong> — aggregating and correlating logs across the environment</li>
  <li><strong>Case management</strong> — structured workflows for tracking investigations</li>
  <li><strong>Threat intelligence</strong> — enriching indicators with external data</li>
  <li><strong>Orchestration</strong> — automating the mechanical parts of analysis</li>
</ul>

<p>Running a home SOC isn’t running an enterprise program. It’s having access to the same class of tooling for learning how these systems work before you need them professionally, practicing incident response against known-bad samples in a controlled environment, conducting vulnerability research with proper case tracking, and analyzing malware without touching anything near actual production data.</p>

<h2 id="thehive-case-management-that-takes-investigation-seriously">TheHive: case management that takes investigation seriously</h2>

<p>TheHive is an open-source incident response platform built around cases — structured investigations that can hold observables, tasks, timeline entries, and links to related cases.</p>

<p>The workflow is familiar to anyone who’s done professional incident response:</p>

<ol>
  <li>Alert comes in (manually or via integration)</li>
  <li>Case opens with relevant observables — IP addresses, domains, file hashes, email headers</li>
  <li>Tasks assigned and tracked within the case</li>
  <li>Observables sent to Cortex for automated enrichment</li>
  <li>Timeline builds as the investigation progresses</li>
  <li>Case closes with documented findings</li>
</ol>

<p>In practice, I use this for malware analysis sessions. Each sample gets a case. Associated infrastructure — C2 servers, distribution domains, related hashes — tracked as observables. Come back to a sample three weeks later and the context is still there.</p>

<h2 id="cortex-the-enrichment-engine">Cortex: the enrichment engine</h2>

<p>Cortex is TheHive’s companion platform. It runs analyzers — integrations with threat intelligence services, OSINT tools, and analysis platforms — against observables submitted from TheHive.</p>

<p>Out of the box:</p>
<ul>
  <li>VirusTotal, MalwareBazaar, abuse.ch</li>
  <li>Shodan, Censys</li>
  <li>WHOIS, DNS, BGP lookups</li>
  <li>URLScan, URLhaus</li>
  <li>Hybrid Analysis, Any.run (API key required)</li>
  <li>Local analysis tools (YARA, strings, capa)</li>
</ul>

<p>From TheHive it’s one click: submit an observable to Cortex, pick the analyzers, get enriched results back in the case timeline. What used to be a sequence of manual lookups across a dozen browser tabs becomes a parallelized automated enrichment run.</p>

<h2 id="the-infrastructure-reality">The infrastructure reality</h2>

<p>TheHive and Cortex have real infrastructure requirements. TheHive needs Elasticsearch or OpenSearch for storage. Cortex needs Docker for its analyzer workers. The combination runs best with 8GB dedicated to the stack.</p>

<p>The <code class="language-plaintext highlighter-rouge">hive.yml</code> playbook deploys both services using roles targeting a dedicated host. Current deployment is CentOS-based, reflecting the original role architecture. Native Fedora and Ubuntu support is on the roadmap.</p>

<p>The <code class="language-plaintext highlighter-rouge">mosburn.elk</code> role provides the Elasticsearch backend TheHive depends on. The <code class="language-plaintext highlighter-rouge">mosburn.filebeat</code> role ships logs from other lab hosts into the ELK stack, giving TheHive’s integrated search a view across the entire lab environment.</p>

<h2 id="the-controlled-research-environment">The controlled research environment</h2>

<p>The most important thing about running security tooling at home is isolation. Analyzing malware or testing exploits on a machine that shares a network with family devices and personal data is not responsible research practice.</p>

<p>The Mosburn Lab handles this through network segmentation and VirtualBox-based isolation. Packer builds clean Fedora and Ubuntu images. Ansible provisions research environments from those images. Session ends, the VM is snapshotted or destroyed. The base image stays clean.</p>

<p>The <code class="language-plaintext highlighter-rouge">mosburn.vbox</code> role manages VirtualBox installation across supported platforms. Research happens inside VMs. The VMs are disposable. The methodology for creating them isn’t.</p>

<h2 id="what-makes-this-sustainable">What makes this sustainable</h2>

<p>Security research infrastructure is only useful if it’s there when you need it. The common failure mode for home security labs: setup is painful enough that you avoid rebuilding after a problem, and eventually the environment is too stale to trust.</p>

<p>The Ansible approach makes rebuild cost low. Full TheHive + Cortex + ELK stack deploys in a single playbook run. The time between “I need a clean research environment” and “I have one” is measured in minutes, not hours.</p>

<p>That’s the dividend from investing upfront in Ansible roles and Molecule tests. The lab doesn’t accumulate debt. When you need it, it works.</p>

<p>And when it doesn’t, you run the playbook again.</p>]]></content><author><name>Michael Osburn</name></author><category term="security" /><category term="infrastructure" /><category term="thehive" /><category term="cortex" /><category term="security-research" /><category term="soc" /><category term="ansible" /><summary type="html"><![CDATA[A Security Operations Center doesn't require a security operations budget. TheHive and Cortex give you professional-grade incident management and threat intelligence tooling — if you're willing to run the infrastructure.]]></summary></entry><entry><title type="html">The Self-Hosted AI Stack: Privacy, Power, and Local Models</title><link href="/2026/05/04/self-hosted-ai-stack/" rel="alternate" type="text/html" title="The Self-Hosted AI Stack: Privacy, Power, and Local Models" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>/2026/05/04/self-hosted-ai-stack</id><content type="html" xml:base="/2026/05/04/self-hosted-ai-stack/"><![CDATA[<p>If you’re doing serious work in 2026, you’re using AI tools. The question isn’t whether — it’s what you’re handing over when you do.</p>

<p>Cloud AI is capable and convenient. It also logs your requests, uses interactions to improve future models, and builds a picture of what you’re working on. For most tasks that tradeoff is fine. For security research, unreleased code, and infrastructure configs with real hostnames and IP ranges in them, it’s not.</p>

<p>The move isn’t to refuse all cloud AI. It’s to be deliberate about what leaves your network and what doesn’t.</p>

<h2 id="the-two-tier-model">The two-tier model</h2>

<p>The Mosburn Lab runs AI at two levels:</p>

<p><strong>Local inference</strong> via Ollama, running open-weight models on-device. No network required, no API keys, no logging anywhere but your own machine. Quality is lower than frontier models for complex reasoning — but for code completion, summarization, and exploratory work, it’s often good enough. And it’s always private.</p>

<p><strong>Cloud access</strong> via CLI tools for frontier models: Claude, Gemini, Codex. Used when the task actually needs frontier-quality reasoning, with the understanding that requests are processed by the provider. The tradeoff is explicit and accepted, not invisible and assumed.</p>

<p>The <code class="language-plaintext highlighter-rouge">mosburn.ai</code> Ansible role manages both tiers. Installs CLI tools via npm across Fedora, Debian, Ubuntu, Arch, and Gentoo. Each tool is independently toggleable. Ollama handled separately — binary install, systemd service registration.</p>

<h2 id="ollama-and-local-models">Ollama and local models</h2>

<p>Ollama is the most approachable entry point to local LLM inference. It handles model download, quantization selection, and serving through a REST API that’s OpenAI-compatible — tools built against the OpenAI API can point at a local Ollama instance without modification. That compatibility matters more than it sounds.</p>

<p>Models I keep running locally:</p>

<ul>
  <li><strong>Llama 3.1 8B</strong> — fast, reasonable quality for most tasks, fits in 8GB VRAM</li>
  <li><strong>Qwen2.5-Coder 7B</strong> — noticeably better than general models for code completion and explanation</li>
  <li><strong>Mistral 7B</strong> — solid for summarization and classification</li>
</ul>

<p>The honest tradeoff: these aren’t GPT-4o or Claude Opus for complex multi-step reasoning. Reviewing a pull request or explaining unfamiliar code — genuinely useful. Designing a distributed system architecture from scratch — reach for a frontier model. The capability gap is real for certain tasks.</p>

<h2 id="claude-cli-and-the-case-for-frontier-access">Claude CLI and the case for frontier access</h2>

<p>Claude is my primary cloud tool. CLI integration means it’s available from any terminal — <code class="language-plaintext highlighter-rouge">claude "explain this error"</code> or <code class="language-plaintext highlighter-rouge">claude "review this Ansible role"</code> without switching to a browser.</p>

<p>What makes Claude specifically useful for infrastructure work is the combination of context length and instruction-following. Feed it an entire Ansible role and ask for a review of idempotence and error handling, and you get useful output. The same task on a local 7B model produces inconsistent results.</p>

<p>Requests go to Anthropic’s infrastructure. I don’t send security research artifacts, unreleased project code, or anything with internal hostnames through cloud tools. That’s not paranoia — that’s just being deliberate about it.</p>

<h2 id="the-mosburnai-role">The mosburn.ai role</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># defaults/main.yml</span>
<span class="na">mosburn_ai_install_claude</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">mosburn_ai_install_gemini</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">mosburn_ai_install_codex</span><span class="pi">:</span> <span class="kc">false</span>
<span class="na">mosburn_ai_install_ollama</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Flip the booleans for what you want. The role installs Node.js if it’s not present and uses npm for the CLI tools. Ollama gets binary installation plus systemd. Runs the same across all supported distributions, which matters because my workstations are Fedora and my test VMs are Ubuntu.</p>

<h2 id="the-data-posture-in-practice">The data posture in practice</h2>

<p>Rules I actually follow:</p>

<ol>
  <li><strong>Security research artifacts</strong> — local only. Malware analysis, exploit research, threat intelligence stays on-device.</li>
  <li><strong>Personal project code</strong> — local for routine tasks, frontier models for architecture review, with awareness that the latter is logged.</li>
  <li><strong>Public or open-source work</strong> — cloud tools freely. No privacy concern with code going public anyway.</li>
  <li><strong>Infrastructure code</strong> — careful. Ansible roles with internal hostnames, IP ranges, and role-specific variable names reveal your environment topology.</li>
</ol>

<p>The Ansible approach makes this easier to enforce. AI tooling consistently available across all hosts means I can make deliberate choices about which tool for which task without worrying about what’s installed where.</p>

<h2 id="whats-next">What’s next</h2>

<p>The integration I want to build is RAG against the lab’s own documentation. Docmost generates docs. Forgejo stores code. A local vector store and embedding model would let me query both without sending anything off-device.</p>

<p>The technology exists — Ollama for embedding, ChromaDB or Qdrant for vector storage. The missing piece is the ingestion pipeline, which is its own interesting infrastructure problem.</p>

<p>More on that when it works.</p>]]></content><author><name>Michael Osburn</name></author><category term="ai" /><category term="infrastructure" /><category term="ollama" /><category term="claude" /><category term="ai" /><category term="llm" /><category term="privacy" /><category term="mosburn.ai" /><summary type="html"><![CDATA[Cloud AI is convenient. Local AI is yours. The Mosburn Lab runs both, on the same workstation, managed by the same Ansible role — with no telemetry, no rate limits, and no training data contribution.]]></summary></entry><entry><title type="html">The Self-Hosted Dev Stack: Forgejo, Redmine, and Docmost</title><link href="/2026/05/01/self-hosted-dev-stack/" rel="alternate" type="text/html" title="The Self-Hosted Dev Stack: Forgejo, Redmine, and Docmost" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>/2026/05/01/self-hosted-dev-stack</id><content type="html" xml:base="/2026/05/01/self-hosted-dev-stack/"><![CDATA[<p>The default move for developer tooling in 2026 is SaaS. GitHub for code, Linear or Jira for tickets, Notion for docs. The integrated experience is real, reliability is generally fine, and the cost sneaks up on you once you’re paying for seat licenses, API tiers, and storage charges.</p>

<p>But cost isn’t why I moved this in-house. The data posture is.</p>

<p>Code repositories have business logic, security research, personal projects — things you’d rather not see crawled for a training dataset. Documentation has architecture decisions, threat models, operational notes. None of that should live exclusively in someone else’s cloud by default.</p>

<p>This isn’t about not trusting GitHub or Notion specifically. It’s about not having a clean answer when someone asks where that data actually lives and who can see it.</p>

<h2 id="forgejo-git-without-the-platform-baggage">Forgejo: git without the platform baggage</h2>

<p>Forgejo is a community fork of Gitea. Full git hosting — web interface, SSH cloning, pull requests, CI hooks, package registry — running on a single modest server.</p>

<p>I picked Forgejo over Gitea for governance reasons. Forgejo is explicitly community-run with a published roadmap. Self-hosted infrastructure should be one acquisition away from nothing, not one acquisition away from a pricing surprise.</p>

<p>In the Mosburn Lab, Forgejo runs on Docker with a PostgreSQL backend. The <code class="language-plaintext highlighter-rouge">mosburn.forgejo</code> role handles container deployment and lifecycle, database provisioning, <code class="language-plaintext highlighter-rouge">app.ini</code> configuration via Ansible template, and Keycloak OAuth2 provider registration via the Forgejo admin API.</p>

<p>That last piece is worth calling out. After the service starts, the role POSTs to <code class="language-plaintext highlighter-rouge">/api/v1/admin/oauth2</code> to register Keycloak as an OIDC provider. The call is idempotent — a 422 response means the provider already exists, which the role treats as success. First run or fiftieth, the end state is the same.</p>

<h2 id="redmine-old-stable-zero-dollars">Redmine: old, stable, zero dollars</h2>

<p>Redmine has been around since 2006. Parts of the UI show it. The plugin ecosystem is extremely stable, the API is well-documented, and it’s been free for almost two decades without that changing.</p>

<p>I’ve used Jira, Linear, Plane, Basecamp. For a home lab and personal projects, the overhead of any of those — including self-hosted options — is more than I need. Issue tracker, time logging, wiki. That’s what I use. Redmine has all of it.</p>

<p>The <code class="language-plaintext highlighter-rouge">mosburn.redmine</code> role is the most complex in the stack. The core Docker image gets extended with a custom <code class="language-plaintext highlighter-rouge">Dockerfile</code> that installs the <code class="language-plaintext highlighter-rouge">redmine_openid_connect</code> plugin at build time:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> redmine:6</span>
<span class="k">RUN </span>apt-get update <span class="nt">-qq</span> <span class="se">\
</span>    <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="nt">--no-install-recommends</span> git <span class="se">\
</span>    <span class="o">&amp;&amp;</span> git clone <span class="nt">--depth</span> 1 <span class="se">\
</span>        https://github.com/CACI-IMG/redmine_openid_connect.git <span class="se">\
</span>        /usr/src/redmine/plugins/redmine_openid_connect <span class="se">\
</span>    <span class="o">&amp;&amp;</span> bundle <span class="nb">install</span> <span class="nt">--without</span> development <span class="nb">test</span>
</code></pre></div></div>

<p>OIDC configuration lives in a <code class="language-plaintext highlighter-rouge">configuration.yml</code> template Ansible renders with the Keycloak issuer URL, realm, and client credentials. Redmine picks it up at startup. Users get a “Sign in with Keycloak” button alongside the local auth form.</p>

<h2 id="docmost-notion-but-yours">Docmost: Notion, but yours</h2>

<p>Docmost is newer — collaborative docs platform, block-based editor, nested pages, real-time collaboration. Think Notion without the pricing page.</p>

<p>It’s the simplest of the three to deploy. The entire configuration is environment variables:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">OIDC_ENABLED</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">OIDC_PROVIDER_NAME</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Mosburn"</span>
<span class="na">OIDC_CLIENT_ID</span><span class="pi">:</span> <span class="s2">"</span><span class="s">docmost"</span>
<span class="na">OIDC_CLIENT_SECRET</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">OIDC_ISSUER</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https:///realms/"</span>
</code></pre></div></div>

<p>Ansible renders these into the Docker Compose environment block. No config files to manage, no plugin installation, no custom image.</p>

<h2 id="the-pattern-underneath-all-three">The pattern underneath all three</h2>

<p>PostgreSQL for persistent storage. Keycloak for authentication. Ansible for deployment, configuration, and lifecycle. systemd managing the Docker Compose layer on the host.</p>

<p>The native installation path — <code class="language-plaintext highlighter-rouge">mosburn_*_use_docker=false</code> — follows the same pattern with the packaging system substituting for Docker. Fedora and Ubuntu task files handle the platform differences; the Ansible interface stays the same.</p>

<p>Updating Forgejo means changing <code class="language-plaintext highlighter-rouge">mosburn_forgejo_version</code> in <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> and rerunning the playbook. Rotating a database password means updating the value and rerunning. The playbook is the single source of truth for what’s running and how it’s configured.</p>

<p>Not just services that work. Services whose state is fully described in version-controlled code and can be reproduced exactly. That’s the goal.</p>]]></content><author><name>Michael Osburn</name></author><category term="infrastructure" /><category term="devtools" /><category term="forgejo" /><category term="redmine" /><category term="docmost" /><category term="self-hosted" /><category term="keycloak" /><summary type="html"><![CDATA[Three services, one identity provider, zero subscriptions. Here's why I moved code hosting, project tracking, and documentation in-house, and what running them on Ansible-managed Docker actually looks like.]]></summary></entry><entry><title type="html">Infrastructure as Code, Test-First: Ansible TDD for the Home Lab</title><link href="/2026/04/29/ansible-tdd-home-lab/" rel="alternate" type="text/html" title="Infrastructure as Code, Test-First: Ansible TDD for the Home Lab" /><published>2026-04-29T00:00:00+00:00</published><updated>2026-04-29T00:00:00+00:00</updated><id>/2026/04/29/ansible-tdd-home-lab</id><content type="html" xml:base="/2026/04/29/ansible-tdd-home-lab/"><![CDATA[<p>The first time I wrote an Ansible role with tests, I thought I was wasting time. Writing a failing test, then code to make it pass, for something as deterministic as “install this package.” The indirection felt pointless.</p>

<p>Third time I caught a regression before it hit a live host, I stopped complaining about it.</p>

<p>TDD for infrastructure is the same discipline as TDD for application code. Same reasons it matters, same objections people have, same payoff when you stick with it.</p>

<h2 id="why-infrastructure-needs-tests-more-than-application-code">Why infrastructure needs tests more than application code</h2>

<p>Application code fails loudly. Stack trace. Broken build. Fast feedback.</p>

<p>Infrastructure fails quietly. A misconfigured service starts fine but behaves wrong. A missing package doesn’t matter until something tries to use it three months later. A task that’s idempotent on Fedora silently breaks on Ubuntu. You find out when you’re running a playbook on a production host at midnight and something’s not where it should be.</p>

<p>Tests close that loop. Running Molecule against a role before pushing is what gives you the same confidence in your infrastructure code that a test suite gives you in application code.</p>

<h2 id="the-95-threshold">The 95% threshold</h2>

<p>The Mosburn Lab holds 95% coverage for both tests and documentation:</p>

<ul>
  <li>Every task in every role has a Molecule test verifying the intended end state</li>
  <li>Every variable in <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> has documentation in the role README</li>
  <li>Every playbook has a comment header explaining what it does and how to run it</li>
</ul>

<p>95% isn’t 100%. The gap is for tasks where testing the outcome is genuinely harder than testing the behavior — waiting on an external API after a service starts, verifying a database migration ran correctly. Those get integration tests rather than unit tests. They still get tests.</p>

<h2 id="the-workflow">The workflow</h2>

<p>For every new role or task:</p>

<ol>
  <li>Write a Molecule scenario asserting the desired end state</li>
  <li>Run Molecule — confirm the test fails for the right reason</li>
  <li>Write the task</li>
  <li>Run Molecule — confirm it passes</li>
  <li>Run again with <code class="language-plaintext highlighter-rouge">--check</code> to verify idempotence</li>
</ol>

<p>Step 2 is not optional. A test that passes before you write the code isn’t testing anything. It’s wrong documentation waiting to burn you.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Start a new role</span>
ansible-galaxy role init roles/mosburn.newrole
<span class="nb">cd </span>roles/mosburn.newrole
molecule init scenario

<span class="c"># Write the test first</span>
vim molecule/default/verify.yml

<span class="c"># Confirm it fails</span>
molecule <span class="nb">test</span>

<span class="c"># Write the role tasks</span>
vim tasks/main.yml

<span class="c"># Confirm it passes</span>
molecule <span class="nb">test</span>

<span class="c"># Confirm idempotence</span>
molecule converge
molecule idempotence
</code></pre></div></div>

<h2 id="what-a-molecule-scenario-looks-like">What a Molecule scenario looks like</h2>

<p>For <code class="language-plaintext highlighter-rouge">mosburn.keycloak</code>, the verify step checks that the Docker service is running, the Keycloak container responds on the configured port, and the systemd unit is enabled:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Verify Keycloak deployment</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">all</span>
  <span class="na">tasks</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Assert keycloak systemd unit is enabled and active</span>
      <span class="na">ansible.builtin.systemd</span><span class="pi">:</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">keycloak</span>
      <span class="na">register</span><span class="pi">:</span> <span class="s">keycloak_service</span>
      <span class="na">failed_when</span><span class="pi">:</span> <span class="pi">&gt;</span>
        <span class="s">keycloak_service.status.ActiveState != 'active' or</span>
        <span class="s">keycloak_service.status.UnitFileState != 'enabled'</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Assert Keycloak HTTP endpoint responds</span>
      <span class="na">ansible.builtin.uri</span><span class="pi">:</span>
        <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://localhost:8080/realms/master"</span>
        <span class="na">status_code</span><span class="pi">:</span> <span class="m">200</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">10</span>
      <span class="na">delay</span><span class="pi">:</span> <span class="m">10</span>

    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Assert docker-compose.yml is present</span>
      <span class="na">ansible.builtin.stat</span><span class="pi">:</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/opt/keycloak/docker-compose.yml</span>
      <span class="na">register</span><span class="pi">:</span> <span class="s">compose_file</span>
      <span class="na">failed_when</span><span class="pi">:</span> <span class="s">not compose_file.stat.exists</span>
</code></pre></div></div>

<p>The native installation path gets its own Molecule scenario — different platform image, verify step checking the Keycloak binary and PostgreSQL instead of Docker.</p>

<h2 id="multi-distro-testing">Multi-distro testing</h2>

<p>Every role supporting Fedora and Ubuntu gets a matrix that tests both:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># molecule/default/molecule.yml</span>
<span class="na">platforms</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">fedora</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">docker.io/fedora:latest</span>
    <span class="na">pre_build_image</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ubuntu</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">docker.io/ubuntu:24.04</span>
    <span class="na">pre_build_image</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>This is where the per-distribution task file pattern earns its keep. <code class="language-plaintext highlighter-rouge">tasks/Fedora.yml</code> and <code class="language-plaintext highlighter-rouge">tasks/Ubuntu.yml</code> test independently in the matrix. A breakage on one platform doesn’t hide behind a passing test on the other.</p>

<h2 id="documentation-coverage">Documentation coverage</h2>

<p>The 95% doc threshold applies to role variables. Every variable in <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> needs a README entry covering what it controls, the default value, and any gotchas.</p>

<p><code class="language-plaintext highlighter-rouge">mosburn.keycloak</code> has 12 variables. All 12 documented. When I need to pass <code class="language-plaintext highlighter-rouge">mosburn_keycloak_native_version</code> to a playbook run six months from now, I won’t be grepping the role source to figure out what it does.</p>

<h2 id="the-discipline-pays-off">The discipline pays off</h2>

<p>These roles deploy across Fedora, Ubuntu, and Gentoo. Some have been running for months. When I add a new distribution target or change a package name, Molecule tells me before a live host does.</p>

<p>That’s the whole point. Tests aren’t fun to write. But a lab that fails silently isn’t something you can trust, and infrastructure you can’t trust is a liability you happen to own.</p>

<p>Write the test first. Always.</p>]]></content><author><name>Michael Osburn</name></author><category term="infrastructure" /><category term="ansible" /><category term="ansible" /><category term="tdd" /><category term="molecule" /><category term="testing" /><category term="infrastructure-as-code" /><summary type="html"><![CDATA[Writing Ansible roles without tests is writing Ansible roles you'll regret. Test-driven development for infrastructure isn't overhead — it's the discipline that makes the rest of the lab trustworthy.]]></summary></entry><entry><title type="html">Identity is Infrastructure: Why Keycloak Comes First</title><link href="/2026/04/22/identity-is-infrastructure/" rel="alternate" type="text/html" title="Identity is Infrastructure: Why Keycloak Comes First" /><published>2026-04-22T00:00:00+00:00</published><updated>2026-04-22T00:00:00+00:00</updated><id>/2026/04/22/identity-is-infrastructure</id><content type="html" xml:base="/2026/04/22/identity-is-infrastructure/"><![CDATA[<p>When I mapped out the Mosburn Lab stack, the list looked like this: Forgejo for code, Redmine for project tracking, Docmost for documentation, TheHive for security research, ELK for log aggregation. Every single one has a login page. Every login page means credentials. And credentials at scale mean password managers, shared secrets, access drift, and eventually the realization that someone’s dev account still has admin rights to something they stopped using a year ago.</p>

<p>Better password hygiene doesn’t fix this. Centralizing authentication before you deploy anything else does.</p>

<h2 id="why-keycloak">Why Keycloak</h2>

<p>Keycloak is Red Hat’s open source identity and access management platform. OAuth2, OpenID Connect, SAML 2.0. Every major application framework can talk to it. Runs fine on modest hardware. Has an admin interface that doesn’t require you to write LDAP queries.</p>

<p>The alternatives I actually considered:</p>

<p><strong>Authentik</strong> is excellent and has a better UI, honestly. I went with Keycloak because the enterprise support story is stronger and the third-party integration docs are more thorough. Either works.</p>

<p><strong>Authelia</strong> is lighter and simpler, but it’s primarily a forward-auth proxy rather than a full identity provider. Fine for protecting static services. Not enough for apps that need to issue tokens to their own APIs.</p>

<p><strong>Kanidm</strong> is interesting — Rust implementation, strong consistency guarantees — but ecosystem support is still catching up.</p>

<p>Keycloak wins on coverage. If a self-hostable application supports OIDC, Keycloak has a documented path for it.</p>

<h2 id="the-architecture">The architecture</h2>

<p>In the Mosburn Lab, Keycloak sits at the center of the auth graph:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                        ┌──────────────────┐
                        │    Keycloak      │
                        │  (mosburn realm) │
                        └────────┬─────────┘
              ┌──────────────────┼──────────────────┐
              │                  │                  │
         ┌────▼───┐        ┌─────▼────┐      ┌─────▼────┐
         │Forgejo │        │ Redmine  │      │ Docmost  │
         └────────┘        └──────────┘      └──────────┘
</code></pre></div></div>

<p>Each application registers as an OIDC client in the <code class="language-plaintext highlighter-rouge">mosburn</code> realm: unique client ID and secret, scoped redirect URIs, claims mapped to what the application actually needs. Users log in once. Keycloak issues a session. Everything else inherits it.</p>

<h2 id="what-this-changes-operationally">What this changes operationally</h2>

<p><strong>Onboarding</strong>: One account in Keycloak. Access to every service that role covers, no per-application user creation required.</p>

<p><strong>Offboarding</strong>: Disable the account. Access revoked everywhere immediately. No checklist of applications to hunt down.</p>

<p><strong>Audit trail</strong>: Authentication events centralized. You can see when anyone last logged into any service from a single view.</p>

<p><strong>Access policy</strong>: Role-based access defined once. Apps trust the claims in the Keycloak token instead of maintaining their own permission models.</p>

<h2 id="the-ansible-approach">The Ansible approach</h2>

<p>The <code class="language-plaintext highlighter-rouge">mosburn.keycloak</code> role deploys via Docker Compose with a PostgreSQL backend and a systemd unit managing the container lifecycle. Structured to support native installation on Fedora and Ubuntu too — useful when you don’t want Docker overhead on a given host.</p>

<p>Configuration in <code class="language-plaintext highlighter-rouge">defaults/main.yml</code>. Realm name, admin credentials, database passwords, hostname — all variables. Nothing hardcoded. Deploy to any inventory host with the right credentials via <code class="language-plaintext highlighter-rouge">-e</code> or Ansible Vault.</p>

<h2 id="deployment-order-is-not-optional">Deployment order is not optional</h2>

<p>Keycloak has to be running before you configure the services authenticating against it. This is why <code class="language-plaintext highlighter-rouge">lab.yml</code> deploys it first:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy Keycloak</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">keycloak</span>
  <span class="na">become</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">roles</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">mosburn.docker_host</span>
    <span class="pi">-</span> <span class="s">mosburn.keycloak</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy Forgejo</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">forgejo</span>
  <span class="s">...</span>
</code></pre></div></div>

<p>The Forgejo role registers Keycloak as an OAuth2 provider via the Forgejo admin API at the end of its run. Keycloak not up? Registration fails. Order is enforced by the playbook, not by hope.</p>

<h2 id="one-thing-id-do-differently">One thing I’d do differently</h2>

<p>Realm and client configuration is currently manual after the initial deploy. The <code class="language-plaintext highlighter-rouge">keycloak_*</code> modules in <code class="language-plaintext highlighter-rouge">community.general</code> can automate client creation, role mapping, and identity provider config — but they need the Keycloak admin REST API available during the Ansible run. Next iteration of the role handles that idempotently, the same way the Forgejo role handles its OAuth2 provider registration. For now, the <code class="language-plaintext highlighter-rouge">changeme</code> client secrets in <code class="language-plaintext highlighter-rouge">defaults/main.yml</code> make it obvious that post-deploy config is still required.</p>

<p>Identity first. Everything else depends on it.</p>]]></content><author><name>Michael Osburn</name></author><category term="infrastructure" /><category term="security" /><category term="keycloak" /><category term="oidc" /><category term="sso" /><category term="identity" /><summary type="html"><![CDATA[Every service you add to your lab creates another password to manage, another login page to remember, another place where access control can drift. The fix is to treat identity as infrastructure — and deploy it first.]]></summary></entry><entry><title type="html">From Chaos to Control: The Case for a Business-Grade Home Lab</title><link href="/2026/04/15/from-chaos-to-control/" rel="alternate" type="text/html" title="From Chaos to Control: The Case for a Business-Grade Home Lab" /><published>2026-04-15T00:00:00+00:00</published><updated>2026-04-15T00:00:00+00:00</updated><id>/2026/04/15/from-chaos-to-control</id><content type="html" xml:base="/2026/04/15/from-chaos-to-control/"><![CDATA[<p>There’s a particular kind of dread that comes with SSH-ing into a machine you set up two years ago. You don’t remember what’s running. You’re not sure which config file is authoritative. The service you need is up, but you couldn’t tell anyone why, and you’re afraid to touch anything in case you break it.</p>

<p>That’s not infrastructure. That’s debt with a power cord.</p>

<p>My personal stuff ran exactly like that for years. Every new project meant another server config done by hand, another package installed with a vague plan to document it later, another thing that would stop working on the next rebuild. Automations I’d forgotten were still running. Critical services with no idea how they got configured.</p>

<p>The breaking point was a threat intelligence setup I needed for some security research. I had the tools, I knew how to use them, and standing up a clean environment took longer than the actual research. Next time I needed it I’d be starting from scratch again. That’s not a workflow. That’s a punishment.</p>

<h2 id="the-business-framing">The business framing</h2>

<p>Something shifts when you start treating home infrastructure the way a small business would treat production systems. Not in budget or complexity — in discipline.</p>

<p>A three-person dev shop doesn’t wing it the way I was winging mine. They have version-controlled config so they know what changed when something breaks. They have reproducible environments so a new person isn’t starting from tribal knowledge. They have centralized access control instead of a spreadsheet of passwords. They know when something stops working before a user does.</p>

<p>None of that requires money. It requires taking it seriously.</p>

<h2 id="the-decision-to-go-all-in-on-ansible">The decision to go all-in on Ansible</h2>

<p>I looked at the options. NixOS is genuinely elegant but it’s a full mental model shift and I wasn’t ready to commit. Kubernetes is the right answer to a different question. Chef and Puppet are more operational overhead than I wanted for a one-person shop.</p>

<p>Ansible fit because it maps to how I already think. Tasks run on hosts, in order, with predictable outcomes. I could write useful automation in an afternoon. The ceiling is high enough that I still haven’t hit it.</p>

<p>The harder call was committing to test-driven development for the roles. Writing a failing test before writing the task felt like friction. It’s not. Every single time I’ve skipped that discipline I’ve paid for it in debugging time. Without exception.</p>

<h2 id="what-business-grade-actually-means-here">What “business-grade” actually means here</h2>

<p>Not PCI compliance in a spare bedroom. It means:</p>

<ul>
  <li><strong>Every host is built from Ansible.</strong> Not “mostly Ansible.” If it’s not in a role, it doesn’t run on my network.</li>
  <li><strong>Environments are disposable.</strong> Any host can be rebuilt from scratch in one playbook run. Packer images give me a clean starting point every time.</li>
  <li><strong>Identity is centralized.</strong> Keycloak handles auth for everything. One set of credentials, OIDC across every service.</li>
  <li><strong>The lab documents itself.</strong> This blog exists because decisions made and then forgotten are the same as decisions never made.</li>
</ul>

<h2 id="whats-coming">What’s coming</h2>

<p>The rest of this series covers the specific choices: why Keycloak has to come before everything else, how TDD changes the way I write roles, what a self-hosted AI development environment actually looks like in practice, and how to run a home security research capability without a SOC budget.</p>

<p>The archaeology phase is over.</p>]]></content><author><name>Michael Osburn</name></author><category term="philosophy" /><category term="infrastructure" /><category term="homelab" /><category term="ansible" /><category term="infrastructure-as-code" /><summary type="html"><![CDATA[Most home labs are archaeological sites — layers of half-remembered decisions, one SSH session away from being unrecoverable. This is the story of building something better.]]></summary></entry></feed>