<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Tinyauth on Haseeb Majid</title>
    <link>https://haseebmajid.dev/tags/tinyauth/</link>
    <description>Recent content in Tinyauth on Haseeb Majid</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Sun, 16 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://haseebmajid.dev/tags/tinyauth/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How I Migrated From Authentik to Pocket ID With TinyAuth on NixOS</title>
      <link>https://haseebmajid.dev/posts/2026-08-16-how-i-migrated-from-authentik-to-pocket-id-with-tinyauth-on-nixos/</link>
      <pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate>
      <author>Me</author>
      <guid>https://haseebmajid.dev/posts/2026-08-16-how-i-migrated-from-authentik-to-pocket-id-with-tinyauth-on-nixos/</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href=&#34;https://haseebmajid.dev/posts/2024-09-06-how-to-setup-authentik-forward-auth-with-traefik-on-nixos&#34;&gt;how I set up Authentik forward auth with Traefik on
NixOS&lt;/a&gt;. It
worked, but over time Authentik got a bit heavy for what I actually needed from it, i.e.
one identity provider and a forward auth in front of a few self-hosted apps. So I swapped
it out for &lt;a href=&#34;https://github.com/pocket-id/pocket-id&#34;&gt;Pocket ID&lt;/a&gt; (a small OIDC provider) and
&lt;a href=&#34;https://github.com/tinyauthhq/tinyauth&#34;&gt;TinyAuth&lt;/a&gt; (a tiny forward auth that talks to it).&lt;/p&gt;
&lt;p&gt;This post is the sequel to that one, showing how I did the migration on NixOS. The
services themselves are NixOS modules, and the OIDC clients and their secrets are declared
in nix too but applied with OpenTofu via Terranix, so the whole thing is declarative end to
end.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A while back I wrote about <a href="/posts/2024-09-06-how-to-setup-authentik-forward-auth-with-traefik-on-nixos">how I set up Authentik forward auth with Traefik on
NixOS</a>. It
worked, but over time Authentik got a bit heavy for what I actually needed from it, i.e.
one identity provider and a forward auth in front of a few self-hosted apps. So I swapped
it out for <a href="https://github.com/pocket-id/pocket-id">Pocket ID</a> (a small OIDC provider) and
<a href="https://github.com/tinyauthhq/tinyauth">TinyAuth</a> (a tiny forward auth that talks to it).</p>
<p>This post is the sequel to that one, showing how I did the migration on NixOS. The
services themselves are NixOS modules, and the OIDC clients and their secrets are declared
in nix too but applied with OpenTofu via Terranix, so the whole thing is declarative end to
end.</p>
<h2 id="why-move-off-authentik">Why move off Authentik</h2>
<p>Authentik does a lot, outposts, flows, property mappings, RBAC, the works. I was only
really using a sliver of it, log in once, forward auth in front of the arr stack and a few
other apps that don&rsquo;t have their own auth. The embedded outpost plus forward auth setup
from the last post worked but it was always a bit fiddly, and Authentik itself was eating a
fair chunk of RAM on my VPS for something I was barely using.</p>
<p>Pocket ID is just OIDC, nothing else. TinyAuth is just forward auth. Two small binaries
instead of one big app, and a much smaller surface for me to keep in my head.</p>
<h2 id="pocket-id">Pocket ID</h2>
<p>Pocket ID is the OIDC provider, so this is the thing my apps and TinyAuth actually log in
against. On NixOS I run it with the
<a href="https://github.com/pocket-id/pocket-id/tree/main/packaging/nix">pocket-id NixOS module</a>,
backed by the local Postgres. The config looks something like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">sops</span><span class="o">.</span><span class="n">secrets</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">pocketid_encryption_key</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">owner</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">pocket-id</span><span class="o">.</span><span class="n">user</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">inherit</span> <span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">pocket-id</span><span class="p">)</span> <span class="n">group</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">mode</span> <span class="o">=</span> <span class="s2">&#34;0400&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">pocketid_static_api_key</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">owner</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">pocket-id</span><span class="o">.</span><span class="n">user</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">inherit</span> <span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">pocket-id</span><span class="p">)</span> <span class="n">group</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">mode</span> <span class="o">=</span> <span class="s2">&#34;0400&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">services</span><span class="o">.</span><span class="n">postgresql</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">ensureDatabases</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;pocketid&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">ensureUsers</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">ensureDBOwnership</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">services</span><span class="o">.</span><span class="n">pocket-id</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">enable</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="s2">&#34;pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">group</span> <span class="o">=</span> <span class="s2">&#34;pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">APP_URL</span> <span class="o">=</span> <span class="s2">&#34;https://id.haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TRUST_PROXY</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">HOST</span> <span class="o">=</span> <span class="s2">&#34;127.0.0.1&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">PORT</span> <span class="o">=</span> <span class="mi">1411</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">DB_CONNECTION_STRING</span> <span class="o">=</span> <span class="s2">&#34;postgres://pocketid@/pocketid?host=/run/postgresql&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">UI_CONFIG_DISABLED</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">ALLOW_USER_SIGNUPS</span> <span class="o">=</span> <span class="s2">&#34;withToken&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">ANALYTICS_DISABLED</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">VERSION_CHECK_DISABLED</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">credentials</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">ENCRYPTION_KEY</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pocketid_encryption_key</span><span class="o">.</span><span class="n">path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">STATIC_API_KEY</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pocketid_static_api_key</span><span class="o">.</span><span class="n">path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">services</span><span class="o">.</span><span class="n">traefik</span><span class="o">.</span><span class="n">dynamicConfigOptions</span><span class="o">.</span><span class="n">http</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">nixicle</span><span class="o">.</span><span class="n">mkTraefikService</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;pocket-id&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span> <span class="o">=</span> <span class="mi">1411</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">subdomain</span> <span class="o">=</span> <span class="s2">&#34;id&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">domain</span> <span class="o">=</span> <span class="s2">&#34;haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>A few things worth pointing out. The <code>ENCRYPTION_KEY</code> and <code>STATIC_API_KEY</code> come from sops,
not from the module&rsquo;s env file, so they live in my secrets repo and not in the nix store.
The DB connection uses the Unix socket (<code>host=/run/postgresql</code>) so Pocket ID doesn&rsquo;t need a
password, it just authenticates as the <code>pocketid</code> postgres role over the socket. And
<code>UI_CONFIG_DISABLED = true</code> means I can&rsquo;t change settings from the web UI, everything has to
go through the nix config, which is what I want.</p>
<p><code>mkTraefikService</code> is a little helper I wrote that spits out the router, service and tls
block for a given subdomain, so I don&rsquo;t have to repeat that boilerplate per app.</p>
<h2 id="tinyauth">TinyAuth</h2>
<p>TinyAuth is the forward auth, same job the Authentik embedded outpost was doing before. It
sits in front of Traefik and redirects you to Pocket ID to log in, then checks each request
after that. I run it as a plain systemd service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="k">let</span>
</span></span><span class="line"><span class="cl">  <span class="n">authPort</span> <span class="o">=</span> <span class="mi">3000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">domain</span> <span class="o">=</span> <span class="s2">&#34;haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">in</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">systemd</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">tinyauth</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;TinyAuth forward-auth (pocket-id OIDC)&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;multi-user.target&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">after</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;network-online.target&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">wants</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;network-online.target&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">environment</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_APPURL</span> <span class="o">=</span> <span class="s2">&#34;https://auth.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_SERVER_PORT</span> <span class="o">=</span> <span class="nb">toString</span> <span class="n">authPort</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_DATABASE_PATH</span> <span class="o">=</span> <span class="s2">&#34;/var/lib/tinyauth/tinyauth.db&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_AUTHURL</span> <span class="o">=</span> <span class="s2">&#34;https://id.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">/authorize&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_TOKENURL</span> <span class="o">=</span> <span class="s2">&#34;https://id.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">/api/oidc/token&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_USERINFOURL</span> <span class="o">=</span> <span class="s2">&#34;https://id.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">/api/oidc/userinfo&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_REDIRECTURL</span> <span class="o">=</span> <span class="s2">&#34;https://auth.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">/api/oauth/callback/pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_SCOPES</span> <span class="o">=</span> <span class="s2">&#34;openid profile email groups&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_PROVIDERS_pocketid_NAME</span> <span class="o">=</span> <span class="s2">&#34;Pocket ID&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">TINYAUTH_OAUTH_AUTOREDIRECT</span> <span class="o">=</span> <span class="s2">&#34;pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">ExecStart</span> <span class="o">=</span> <span class="n">lib</span><span class="o">.</span><span class="n">getExe</span> <span class="n">pkgs</span><span class="o">.</span><span class="n">tinyauth</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">EnvironmentFile</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sops</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">tinyauth_env</span><span class="o">.</span><span class="n">path</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">User</span> <span class="o">=</span> <span class="s2">&#34;tinyauth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">Group</span> <span class="o">=</span> <span class="s2">&#34;tinyauth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">StateDirectory</span> <span class="o">=</span> <span class="s2">&#34;tinyauth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">Restart</span> <span class="o">=</span> <span class="s2">&#34;on-failure&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The <code>tinyauth_env</code> sops secret holds the OAuth client id and secret for the TinyAuth client
I made in Pocket ID (more on how that gets there in a bit).</p>
<p>Then the Traefik side is just a forwardAuth middleware plus a router so the callback URL is
reachable:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">services</span><span class="o">.</span><span class="n">traefik</span><span class="o">.</span><span class="n">dynamicConfigOptions</span><span class="o">.</span><span class="n">http</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">middlewares</span><span class="o">.</span><span class="n">tinyauth</span><span class="o">.</span><span class="n">forwardAuth</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">address</span> <span class="o">=</span> <span class="s2">&#34;http://127.0.0.1:</span><span class="si">${</span><span class="nb">toString</span> <span class="n">authPort</span><span class="si">}</span><span class="s2">/api/auth/traefik&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">authResponseHeaders</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Remote-User&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Remote-Name&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Remote-Email&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Remote-Groups&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">routers</span><span class="o">.</span><span class="n">tinyauth</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">entryPoints</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;websecure&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">rule</span> <span class="o">=</span> <span class="s2">&#34;Host(`auth.</span><span class="si">${</span><span class="n">domain</span><span class="si">}</span><span class="s2">`)&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">service</span> <span class="o">=</span> <span class="s2">&#34;tinyauth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">tls</span><span class="o">.</span><span class="n">certResolver</span> <span class="o">=</span> <span class="s2">&#34;letsencrypt&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">services</span><span class="o">.</span><span class="n">tinyauth</span><span class="o">.</span><span class="n">loadBalancer</span><span class="o">.</span><span class="n">servers</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span> <span class="n">url</span> <span class="o">=</span> <span class="s2">&#34;http://127.0.0.1:</span><span class="si">${</span><span class="nb">toString</span> <span class="n">authPort</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This is the bit that replaces the <code>authentik</code> middleware from the old post. The address is
TinyAuth&rsquo;s <code>/api/auth/traefik</code> endpoint instead of the Authentik outpost on <code>9443</code>, and the
headers it sends back are <code>Remote-User</code> and friends instead of the <code>X-authentik-*</code> ones.
Same idea, less moving parts.</p>
<h2 id="putting-an-app-behind-it">Putting an app behind it</h2>
<p>To protect an app I just add the <code>tinyauth</code> middleware to that app&rsquo;s Traefik router, same as
I used to add <code>authentik</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">services</span><span class="o">.</span><span class="n">traefik</span><span class="o">.</span><span class="n">dynamicConfigOptions</span><span class="o">.</span><span class="n">http</span><span class="o">.</span><span class="n">routers</span><span class="o">.</span><span class="n">sonarr</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">entryPoints</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;websecure&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">rule</span> <span class="o">=</span> <span class="s2">&#34;Host(`sonarr.bare.homelab.haseebmajid.dev`)&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">service</span> <span class="o">=</span> <span class="s2">&#34;sonarr&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">tls</span><span class="o">.</span><span class="n">certResolver</span> <span class="o">=</span> <span class="s2">&#34;letsencrypt&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">middlewares</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;tinyauth&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Hit the app unauthenticated and TinyAuth bounces you to Pocket ID to log in, then back.
That&rsquo;s the whole flow.</p>
<h2 id="provisioning-the-oidc-clients-with-terranix">Provisioning the OIDC clients with Terranix</h2>
<p>This is the bit I&rsquo;m most happy with. With Authentik I used to click around the admin UI to
make a new client for each app. With Pocket ID I declare the clients in nix and apply them
with OpenTofu (via Terranix), so they&rsquo;re version controlled and reproducible.</p>
<p>The clients live in <code>infra/terranix/modules/pocketid-apps.nix</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="k">let</span>
</span></span><span class="line"><span class="cl">  <span class="n">apps</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">tinyauth</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;TinyAuth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">client_id</span> <span class="o">=</span> <span class="s2">&#34;tinyauth&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">launch_url</span> <span class="o">=</span> <span class="s2">&#34;https://auth.haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">callback_urls</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://auth.haseebmajid.dev/api/oauth/callback/pocketid&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">is_public</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">pkce_enabled</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">lettucego</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;LettuceGo&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">client_id</span> <span class="o">=</span> <span class="s2">&#34;lettucego&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">launch_url</span> <span class="o">=</span> <span class="s2">&#34;https://lettucego.haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">callback_urls</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://lettucego.haseebmajid.dev/callback&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">];</span>
</span></span><span class="line"><span class="cl">      <span class="n">is_public</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">pkce_enabled</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># ...gothreads, grafana, tandoor, papra, etc.</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">mkClient</span> <span class="o">=</span> <span class="n">key</span><span class="p">:</span> <span class="n">cfg</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">resource</span><span class="o">.</span><span class="n">pocketid_client</span><span class="o">.</span><span class="si">${</span><span class="n">key</span><span class="si">}</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">inherit</span> <span class="p">(</span><span class="n">cfg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span>
</span></span><span class="line"><span class="cl">        <span class="n">client_id</span>
</span></span><span class="line"><span class="cl">        <span class="n">callback_urls</span>
</span></span><span class="line"><span class="cl">        <span class="n">is_public</span>
</span></span><span class="line"><span class="cl">        <span class="n">pkce_enabled</span>
</span></span><span class="line"><span class="cl">        <span class="n">launch_url</span>
</span></span><span class="line"><span class="cl">        <span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">output</span><span class="o">.</span><span class="s2">&#34;</span><span class="si">${</span><span class="n">key</span><span class="si">}</span><span class="s2">_client_id&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{pocketid_client.</span><span class="si">${</span><span class="n">key</span><span class="si">}</span><span class="s2">.id}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">output</span><span class="o">.</span><span class="s2">&#34;</span><span class="si">${</span><span class="n">key</span><span class="si">}</span><span class="s2">_client_secret&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{pocketid_client.</span><span class="si">${</span><span class="n">key</span><span class="si">}</span><span class="s2">.client_secret}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">sensitive</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">in</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">imports</span> <span class="o">=</span> <span class="nb">builtins</span><span class="o">.</span><span class="n">attrValues</span> <span class="p">(</span><span class="nb">builtins</span><span class="o">.</span><span class="n">mapAttrs</span> <span class="n">mkClient</span> <span class="n">apps</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Each entry becomes a <code>pocketid_client</code> Terraform resource, so <code>tofu apply</code> creates the
client in Pocket ID and prints the id and secret as Terraform outputs.</p>
<p>The clever bit is the next file. Rather than copying those secrets into each app&rsquo;s sops file
by hand, I push them straight into <a href="https://openbao.org/">OpenBao</a> (a Vault fork) KV from the
same Terraform run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="k">let</span>
</span></span><span class="line"><span class="cl">  <span class="n">apps</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;goroutinely&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;gothreads&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;lettucego&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;sure&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;tinyauth&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;grafana&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">mkSecret</span> <span class="o">=</span> <span class="n">app</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">resource</span><span class="o">.</span><span class="n">vault_kv_secret_v2</span><span class="o">.</span><span class="s2">&#34;</span><span class="si">${</span><span class="n">app</span><span class="si">}</span><span class="s2">_oidc&#34;</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">mount</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{vault_mount.kv.path}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;apps/</span><span class="si">${</span><span class="n">app</span><span class="si">}</span><span class="s2">/oidc&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="n">data_json</span> <span class="o">=</span> <span class="nb">builtins</span><span class="o">.</span><span class="n">toJSON</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">client_id</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{pocketid_client.</span><span class="si">${</span><span class="n">app</span><span class="si">}</span><span class="s2">.id}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">client_secret</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{pocketid_client.</span><span class="si">${</span><span class="n">app</span><span class="si">}</span><span class="s2">.client_secret}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="k">in</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">resource</span><span class="o">.</span><span class="n">vault_mount</span><span class="o">.</span><span class="n">kv</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="s2">&#34;kv&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">type</span> <span class="o">=</span> <span class="s2">&#34;kv&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">options</span><span class="o">.</span><span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;2&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">imports</span> <span class="o">=</span> <span class="nb">map</span> <span class="n">mkSecret</span> <span class="n">apps</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>So one <code>tofu apply</code> makes the client in Pocket ID and writes its <code>client_id</code> and
<code>client_secret</code> into OpenBao at <code>kv/apps/&lt;app&gt;/oidc</code>. The apps then pull from OpenBao at
runtime, no secrets sitting in git, no manual copy paste. Add a new app, append one entry to
both lists, apply, done.</p>
<h2 id="from-nix-to-opentofu">From nix to OpenTofu</h2>
<p>So there are two declarative layers doing different jobs. Pocket ID and TinyAuth are
NixOS services, so they&rsquo;re declared in nix and applied with <code>nixos-rebuild</code>, same as the
rest of the box. The OIDC clients and their secrets can&rsquo;t really be a NixOS module, they
live behind Pocket ID&rsquo;s and OpenBao&rsquo;s APIs, so I declare those in nix too but as Terranix,
which compiles down to terraform JSON, and then OpenTofu applies them.</p>
<p>The whole Terranix config is just one file that imports three modules:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="c1"># infra/terranix/prod.nix</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">imports</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="sr">./modules/terraform.nix</span>
</span></span><span class="line"><span class="cl">    <span class="sr">./modules/pocketid-apps.nix</span>
</span></span><span class="line"><span class="cl">    <span class="sr">./modules/openbao-secrets.nix</span>
</span></span><span class="line"><span class="cl">  <span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>pocketid-apps.nix</code> and <code>openbao-secrets.nix</code> are the ones from the last section. The
third one, <code>terraform.nix</code>, is the boring bit that sets up the providers and variables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">terraform</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">required_version</span> <span class="o">=</span> <span class="s2">&#34;&gt;= 1.0&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">backend</span><span class="o">.</span><span class="n">local</span> <span class="o">=</span> <span class="p">{</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="n">required_providers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">pocketid</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">source</span> <span class="o">=</span> <span class="s2">&#34;trozz/pocketid&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 0.1&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">      <span class="n">vault</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">source</span> <span class="o">=</span> <span class="s2">&#34;hashicorp/vault&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 4.0&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">provider</span><span class="o">.</span><span class="n">pocketid</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">base_url</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{var.pocketid_base_url}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">api_token</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{var.pocketid_api_token}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">provider</span><span class="o">.</span><span class="n">vault</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">address</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{var.openbao_address}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">token</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\$</span><span class="s2">{var.openbao_token}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">skip_child_token</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">variable</span><span class="o">.</span><span class="n">pocketid_base_url</span><span class="o">.</span><span class="n">type</span> <span class="o">=</span> <span class="s2">&#34;string&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">variable</span><span class="o">.</span><span class="n">pocketid_api_token</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">type</span> <span class="o">=</span> <span class="s2">&#34;string&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sensitive</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">variable</span><span class="o">.</span><span class="n">openbao_address</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">type</span> <span class="o">=</span> <span class="s2">&#34;string&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">default</span> <span class="o">=</span> <span class="s2">&#34;https://openbao.homelab.haseebmajid.dev&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">variable</span><span class="o">.</span><span class="n">openbao_token</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">type</span> <span class="o">=</span> <span class="s2">&#34;string&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sensitive</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The <code>pocketid</code> provider (<code>trozz/pocketid</code>) talks to Pocket ID&rsquo;s admin API, the <code>vault</code>
provider talks to OpenBao. Both tokens come in as terraform variables, i.e. env vars at
apply time, so they&rsquo;re not in the nix config.</p>
<p>To actually run it I have a <code>go-task</code> task that turns the nix into terraform JSON and then
hands it to OpenTofu:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># tasks/tofu.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tasks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">generate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cmds</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">mkdir -p infra/terranix/generated/prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">terranix infra/terranix/prod.nix &gt; infra/terranix/generated/prod/config.tf.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apply</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">deps</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">generate]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dir</span><span class="p">:</span><span class="w"> </span><span class="l">infra/terranix/generated/prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cmds</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">tofu init</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">tofu apply</span><span class="w">
</span></span></span></code></pre></div><p>So <code>task tofu:apply</code> runs <code>terranix prod.nix</code> to spit out a <code>config.tf.json</code>, then <code>tofu init &amp;&amp; tofu apply</code> creates the clients in Pocket ID and pushes the secrets into OpenBao.
No <code>terraform apply</code> with hand-written HCL, the HCL is generated from nix, so the clients
and the NixOS services stay in sync and in the same repo.</p>
<p>Both <code>opentofu</code> and <code>terranix</code> are just in the flake&rsquo;s devShell, so anyone cloning the repo
gets them for free, no separate terraform install to manage:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="c1"># flake devShell</span>
</span></span><span class="line"><span class="cl"><span class="n">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...sops, age, git, etc.</span>
</span></span><span class="line"><span class="cl">  <span class="n">opentofu</span>
</span></span><span class="line"><span class="cl">  <span class="n">terranix</span>
</span></span><span class="line"><span class="cl">  <span class="n">go-task</span>
</span></span><span class="line"><span class="cl"><span class="p">];</span>
</span></span></code></pre></div><h2 id="what-i-lost-and-what-i-gained">What I lost and what I gained</h2>
<p>The main thing I gave up is Authentik&rsquo;s flows, i.e. the ability to build custom login /
recovery / enrolment flows in the UI. I never actually used those, so I don&rsquo;t miss them.
Pocket ID also doesn&rsquo;t do groups-based access control to the same degree, but TinyAuth&rsquo;s
<code>Remote-Groups</code> header lets me do the simple version in Traefik if I ever need it.</p>
<p>What I gained is a much smaller footprint on the VPS, no more clicking in an admin UI to
register clients, and the whole identity setup (provider, forward auth, client
provisioning, secret distribution) is now declared in nix and applied with OpenTofu. That
alone was worth the swap for me.</p>
<h2 id="appendix">Appendix</h2>
<ul>
<li>My home lab Nix config
<ul>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/modules/aspects/services/pocket-id.nix">Pocket ID config</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/modules/aspects/services/tinyauth.nix">TinyAuth config</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/infra/terranix/modules/pocketid-apps.nix">Pocket ID clients (Terranix)</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/infra/terranix/modules/openbao-secrets.nix">OpenBao OIDC secrets (Terranix)</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/infra/terranix/modules/terraform.nix">Terranix providers + vars</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/infra/terranix/prod.nix">Terranix entry point</a></li>
<li><a href="https://gitlab.com/hmajid2301/nixicle/-/blob/d3dd29ed/tasks/tofu.yml">OpenTofu go-task tasks</a></li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
