<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yoann Saunier</title>
    <description>The latest articles on DEV Community by Yoann Saunier (@ysaunier).</description>
    <link>https://dev.to/ysaunier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2456004%2F762205e6-f216-46ba-bb4e-cbf1b108677c.jpg</url>
      <title>DEV Community: Yoann Saunier</title>
      <link>https://dev.to/ysaunier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ysaunier"/>
    <language>en</language>
    <item>
      <title>My Homelab Journey: From a Pi-hole to a Fully SSO'd Home</title>
      <dc:creator>Yoann Saunier</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:19:01 +0000</pubDate>
      <link>https://dev.to/ysaunier/my-homelab-journey-from-a-pi-hole-to-a-fully-ssod-home-4e</link>
      <guid>https://dev.to/ysaunier/my-homelab-journey-from-a-pi-hole-to-a-fully-ssod-home-4e</guid>
      <description>&lt;p&gt;It started small. I just wanted ads and trackers gone from every device on my home network. It ended up somewhere much bigger: a real internal Certificate Authority, single sign-on across half a dozen self-hosted apps, and a late-night debugging session where I accidentally locked myself out of my own media server.&lt;/p&gt;

&lt;p&gt;Here's the whole journey, warts and all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started
&lt;/h2&gt;

&lt;p&gt;The trigger was mundane. Too many ads, too much tracking, and a general itch to understand what was happening on my home network. &lt;a href="https://pi-hole.net/" rel="noopener noreferrer"&gt;Pi-hole&lt;/a&gt; is the obvious entry point for this: a DNS sinkhole that blocks known ad and tracker domains for every device on the network, no per-device setup needed.&lt;/p&gt;

&lt;p&gt;I didn't want to commit a spare machine to it right away, so I ran it as a Docker container on my main Windows PC under WSL2. That was good enough to prove the concept and get comfortable with the tooling, but not something I'd trust as the DNS server for an entire household. If that PC was off, every device lost DNS resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going real: a dedicated home server
&lt;/h2&gt;

&lt;p&gt;Once I knew I wanted this long-term, I moved Pi-hole onto a dedicated Raspberry Pi 4, running natively with a static LAN IP. And once it was there, other things started attaching themselves to it: Nginx Proxy Manager as a reverse proxy in front of everything, Home Assistant, and eventually a full identity provider.&lt;/p&gt;

&lt;p&gt;That one small Pi quietly became the de facto server for the household. I don't think that's unusual. You solve one problem and end up with a platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fczt4xxv13ad1rvwtwea9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fczt4xxv13ad1rvwtwea9.png" alt=" " width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The DHCP surprise
&lt;/h2&gt;

&lt;p&gt;Not long after the move, one of my personal machines, a Wi-Fi-only laptop with no Ethernet port, kept getting a random DHCP lease instead of the static IP I'd reserved for it. I assumed the culprit was the household's stock ISP router, since that's the usual DHCP authority in most home setups, and spent a while chasing settings there.&lt;/p&gt;

&lt;p&gt;Wrong tree entirely. Pi-hole ships its own embedded DHCP server, and it was the one answering, with zero static reservations configured. Two things stuck with me from this:&lt;/p&gt;

&lt;p&gt;Don't assume which component owns a responsibility. Check what's active before you go debugging somewhere else. In my case that meant reading Pi-hole's own DHCP config instead of trusting my mental model of the network.&lt;/p&gt;

&lt;p&gt;A network can silently run two DHCP servers at once. If the router's DHCP is also still enabled, you've got two authorities racing to answer the same requests. I still need to go verify and settle that properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wanting real logins: enter an identity provider
&lt;/h2&gt;

&lt;p&gt;Once several apps were running behind the reverse proxy, "one shared admin password copy-pasted into three different login forms" started to feel obviously wrong. I stood up &lt;a href="https://kanidm.com/" rel="noopener noreferrer"&gt;Kanidm&lt;/a&gt; as a self-hosted identity provider and picked a simple policy going forward: OIDC first. Any new app gets single sign-on if it can support it. Local passwords become the fallback, not the primary path.&lt;/p&gt;

&lt;p&gt;Simple to write down. Much less simple to apply across a pile of self-hosted apps that were never designed with each other in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTTPS problem nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Everything above was running on an internal pseudo-domain: fine for browsers, useless the moment you need real TLS. Let's Encrypt needs proof of control over a real, publicly delegated DNS zone, and a private internal domain doesn't qualify.&lt;/p&gt;

&lt;p&gt;So I stood up my own internal Certificate Authority. One root CA, one leaf certificate covering every internal hostname, imported once into each device's trust store. Two mistakes along the way turned into the lessons that stuck.&lt;/p&gt;

&lt;p&gt;The first: a literal &lt;code&gt;*.home&lt;/code&gt;-style wildcard doesn't work. RFC 6125 forbids matching a wildcard against a single-label base domain, the same rule that stops &lt;code&gt;*.com&lt;/code&gt; from matching &lt;code&gt;foo.com&lt;/code&gt;. Browsers didn't care. Strict TLS stacks like OpenSSL and .NET rejected it outright. The fix was to list every hostname explicitly in the certificate's SAN field instead of leaning on the wildcard.&lt;/p&gt;

&lt;p&gt;The second was worse, because it hid longer. The CA certificate itself was missing its Key Usage extension. &lt;code&gt;basicConstraints=CA:TRUE&lt;/code&gt; alone satisfied browsers, curl, and .NET, but Python's &lt;code&gt;ssl&lt;/code&gt; module (used by one of my OIDC integrations) refused the whole chain with a bare &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt;, because it enforces a stricter RFC 5280 check that the other clients happen to skip. Reissuing the CA cert with the right key usage flags fixed it, same private key, so nothing downstream needed reissuing.&lt;/p&gt;

&lt;p&gt;That second bug is exactly the kind that looks fine in every quick check and then breaks a specific client months later. So I wrote a small test suite that does a real strict-mode TLS handshake against every internal hostname and asserts it validates cleanly. Cheap insurance against ever shipping that regression twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring up SSO, app by app
&lt;/h2&gt;

&lt;p&gt;With TLS sorted, I went through my self-hosted apps one at a time, wiring each into the identity provider over OIDC. The pattern that emerged pretty fast: every app has at least one undocumented quirk, and status codes lie to you more often than you'd like.&lt;/p&gt;

&lt;p&gt;Jellyfin's SSO plugin (&lt;code&gt;jellyfin-plugin-sso&lt;/code&gt;) didn't render a login button at all for real users. Not a bug, just an actual, poorly surfaced design choice: the button had to be added by hand as raw HTML in Jellyfin's own branding settings. The plugin itself turned out to be archived upstream, the maintainer's last note something like "tired of working on this after all the years."&lt;/p&gt;

&lt;p&gt;Home Assistant and Kavita both had their OIDC redirect URI silently drift from plain HTTP to HTTPS once I put a TLS certificate in front of them. Kanidm's client config still expected the old scheme, so every real login attempt failed with a generic, unhelpful error.&lt;/p&gt;

&lt;p&gt;That generic error page, it turns out, returns HTTP 200. Not an error status. Which means checking that a redirect returns a 2xx or 3xx is not a valid way to confirm an OIDC flow works. The only reliable check is reading the real redirect URI parameter, or clicking through the login button as a real user would.&lt;/p&gt;

&lt;p&gt;None of this was exotic. It's the kind of thing you only catch by testing the golden path end to end instead of trusting a green checkmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  When SSO doesn't fit: the app that stayed local
&lt;/h2&gt;

&lt;p&gt;Not everything cooperated. n8n, the workflow automation tool I self-host, has no usable free-tier SSO or LDAP support. Both are gated behind an enterprise license. I tried working around it with &lt;code&gt;oauth2-proxy&lt;/code&gt; as a forward-auth layer in front of it, and got it technically working: unauthenticated requests bounced to Kanidm, authenticated ones passed through.&lt;/p&gt;

&lt;p&gt;Except n8n still kept its own separate local login screen, and it never went away. So the end result was two logins instead of one, which is exactly the friction single sign-on is supposed to remove. I ripped the whole workaround back out. Sometimes the honest answer is "this app doesn't get SSO," and that's a fine place to land instead of shipping something worse just to say every app is integrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The late-night bug: SSO login silently revoking my own admin rights
&lt;/h2&gt;

&lt;p&gt;This is the one I'll remember longest, because it only showed up once I used the thing for real.&lt;/p&gt;

&lt;p&gt;I'd finished wiring Jellyfin into SSO and tested it, successfully, with a service account, then called it done. Weeks later, logging into my own personal account for the first time through SSO, the admin dashboard just wasn't there anymore. No error, no warning: it was just gone.&lt;/p&gt;

&lt;p&gt;The SSO plugin had an authorization sync feature: on every login, it re-derives the local account's admin flag from role claims coming out of Kanidm. I'd never configured any role mapping, because I'd never needed to. Every login I'd tested up to that point used a service account that had never had admin rights to lose in the first place. My real admin account, logging in through SSO for the first time, got quietly demoted by a feature I didn't even know was doing anything.&lt;/p&gt;

&lt;p&gt;Fixing it meant briefly stopping the app, backing up its database, editing the permission flag directly in its SQLite file (after checking, table by table, which column really meant "is admin"), and turning off the auto-sync feature that caused it. I chose "stop touching permissions automatically" over "configure a role mapping nobody in a two or three person household needs."&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;A few things I'd tell myself at the start, if I could.&lt;/p&gt;

&lt;p&gt;Status codes lie more often than you'd like. Several of these systems return a technically successful-looking response for a state that's broken underneath, so now I don't trust anything short of clicking through the real flow myself.&lt;/p&gt;

&lt;p&gt;I also stopped assuming which piece owns a given job. Twice now the real answer wasn't the component I'd have bet on, and both times a few minutes of checking beat an hour of debugging the wrong system.&lt;/p&gt;

&lt;p&gt;Back up before you touch a database by hand. Every time I ended up editing one directly, it was because the API path I wanted didn't exist or wasn't reachable anymore, and having a timestamped copy first turned a nervous fix into a confident one.&lt;/p&gt;

&lt;p&gt;Not every app needs to join the SSO party, either. One app in this stack deliberately stayed on local auth, because forcing SSO onto it made the experience worse, not better, and that's a fine trade to make.&lt;/p&gt;

&lt;p&gt;And test with an account that has something real to lose. The scariest bugs only show up on a login you care about, not a throwaway one made just to tick a box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is still very much in progress:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated backups that work end to end. Right now the transfer hangs partway through, a Windows and SSH interop issue I haven't cracked yet.&lt;/li&gt;
&lt;li&gt;Firewall and SSH hardening, deliberately not yet applied. I'd rather wait for a clear-headed go/no-go than treat it as a spare-afternoon project on a box the whole household's DNS depends on.&lt;/li&gt;
&lt;li&gt;Distributing the internal CA certificate more broadly, so more devices can validate internal TLS without manual trust-store work on each one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something similar, or you've hit your own version of the "green checkmark lied to me" bug, I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosted</category>
      <category>docker</category>
      <category>security</category>
    </item>
  </channel>
</rss>
