<?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>Posts on Blog</title><link>/posts/</link><description>Recent content in Posts on Blog</description><generator>Hugo -- 0.150.0</generator><language>en-us</language><copyright>2025, Mattia Müggler</copyright><lastBuildDate>Sat, 24 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>How to connect to Kubernetes applications through Tailscale?</title><link>/post/how-to-connect-to-kubernetes-applications-through-tailscale/</link><pubDate>Sat, 24 Jan 2026 00:00:00 +0000</pubDate><guid>/post/how-to-connect-to-kubernetes-applications-through-tailscale/</guid><description>&lt;h2 id="why-i-want-to-use-tailscale"&gt;Why I want to use Tailscale&lt;/h2&gt;
&lt;p&gt;Tailscale is a VPN service that makes it easy to create secure networks and connections between devices. The main reason
I want to use Tailscale is that I need a secure and private way to access services that should not be publicly
accessible. Tailscale allows me to create a virtual private network (VPN) where my devices and services can communicate
securely over the internet, without exposing them to the public.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="why-i-want-to-use-tailscale">Why I want to use Tailscale</h2>
<p>Tailscale is a VPN service that makes it easy to create secure networks and connections between devices. The main reason
I want to use Tailscale is that I need a secure and private way to access services that should not be publicly
accessible. Tailscale allows me to create a virtual private network (VPN) where my devices and services can communicate
securely over the internet, without exposing them to the public.</p>
<h2 id="how-to-create-your-own-tailscale-account-with-openid-connect-oidc">How to create your own Tailscale account with OpenId Connect (OIDC)?</h2>
<p>This use case has already been described in my previous blog
post <a href="/post/how-to-set-up-tailscale-with-zitadel/">How to set up a Tailscale account with OIDC?</a>.</p>
<h2 id="how-to-deploy-tailscale-operator-in-kubernetes">How to deploy Tailscale Operator in Kubernetes?</h2>
<p>I decided to deploy the Tailscale Operator which manages the Tailscale Custom Resource Definitions (CRDs) in my
Kubernetes cluster. For the deployment, I used the Helm chart provided by Tailscale. However, I created it as a rainbow
chart to include my custom values like syncing the credentials from my 1Password vault. Here is an example of my
<code>values.yaml</code> file:</p>
<p><strong>Chart.yaml</strong></p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v2</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tailscale</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">1.0.0</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">dependencies</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tailscale-operator</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">1.92.5</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">repository</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">https://pkgs.tailscale.com/helmcharts</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">alias</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tso</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><strong>values.yaml</strong></p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">tailscaleOperator</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">oauth</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">onePasswordItemPath</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;vaults/&lt;my-vault-uuid&gt;/items/&lt;my-item-uuid&gt;&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">restartOnChange</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">tso</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">operatorConfig</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">repository</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ghcr.io/tailscale/k8s-operator</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#8b949e;font-style:italic"># tag: &#34;1.92.5&#34; # will be set by the dependency</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">pullPolicy</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">IfNotPresent</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">resources</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">limits</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">cpu</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">100m</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">memory</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">128Mi</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">requests</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">cpu</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">50m</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">memory</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">64Mi</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="update-your-access-control-settings-in-tailscale">Update your access control settings in Tailscale</h3>
<p>To allow the Tailscale Operator to join your Tailscale network, you need to update your access control settings in
Tailscale. Navigate to the <a href="https://login.tailscale.com/admin/acls/visual/tags">Tailscale Admin Console - Tags section</a>.
Add a new tag called <code>tag:k8s-operator</code> and allow devices with this tag to join your network.</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;tagOwners&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;tag:k8s-operator&#34;</span>: [],
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;tag:k8s&#34;</span>: [
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#34;tag:k8s-operator&#34;</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="how-to-get-the-oauth-credentials">How to get the OAuth credentials?</h3>
<p>Open Tailscale and navigate to the &ldquo;Settings&rdquo; page. Under
the <a href="https://login.tailscale.com/admin/settings/trust-credentials">&ldquo;Trust credentials&rdquo;</a> section, create new credentials.
Choose &ldquo;OAuth&rdquo; as the type and enter &ldquo;Kubernetes Operator&rdquo; as the description.</p>
<p>As you can see, I set the <code>onePasswordItemPath</code> to sync the OAuth credentials from my 1Password vault. This way, I can
securely manage my Tailscale OAuth credentials without hardcoding them in the Helm values. Therefore, I want that the
operator reads the credentials from a secret which is synced from 1Password. My secret looks like this:</p>
<blockquote>
<p>Consider that the secret must be called <code>operator-oauth</code> and must be created in the same namespace where the
Tailscale. Also, the keys must be named <code>client_id</code> and
<code>client_secret</code>. <a href="https://github.com/tailscale/tailscale/blob/main/cmd/k8s-operator/deploy/chart/values.yaml">Read more</a></p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">data</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">client_id</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&lt;base-64-encoded-client-id&gt;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">client_secret</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&lt;base-64-encoded-client-secret&gt;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Secret</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">operator-oauth</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">namespace</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tailscale</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">type</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Opaque</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="granting-additional-permissions-to-the-tailscale-operator">Granting additional permissions to the Tailscale Operator</h3>
<p>One small note, you need to give the Tailscale operator a bit more permissions to create the necessary resources.
Therefore, I also added a namespace resource in my <code>templates</code> folder of the rainbow chart:</p>
<blockquote>
<p>I use argo-cd hooks to ensure that the namespace is created before any other resources are applied. If you are using
helm, you can use helm hooks instead.</p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Namespace</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tailscale</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">annotations</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">argocd.argoproj.io/hook</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">PreSync</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># ensures the namespace is created before other resources are applied</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">labels</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">pod-security.kubernetes.io/enforce</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">privileged</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># adds the privileged policy to the namespace</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>After creating the rainbow chart, I deployed it to my Kubernetes cluster using Argo CD.</p>
<h2 id="how-to-access-kubernetes-applications-through-tailscale">How to access Kubernetes applications through Tailscale?</h2>
<p>To access Kubernetes applications through Tailscale, I used the Tailscale Ingress Controller. This controller allows me
to expose my Kubernetes services to the Tailscale network securely. Here is an example of how I configured the Ingress
Controller:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">26
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">networking.k8s.io/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Ingress</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">my-ingress</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">defaultBackend</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">service</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">my-service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">port</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">number</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8000</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">ingressClassName</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">tailscale</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># ensures that the Tailscale Ingress Controller handles this ingress</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">rules</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">host</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">myservice.example.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">http</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">paths</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span>- <span style="color:#7ee787">path</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">/</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">pathType</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Prefix</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">backend</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">service</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">my-service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span><span style="color:#7ee787">port</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                  </span><span style="color:#7ee787">number</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8000</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">tls</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">hosts</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#a5d6ff">myservice.example.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">secretName</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">my-service-tls</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="accessing-the-application-through-tailscale">Accessing the application through Tailscale</h2>
<p>To access my Kubernetes application through Tailscale, I first need to ensure that my Tailscale client is running on my
client. Then, I have to create a CNAME DNS record that points to the Tailscale IP address of my Kubernetes cluster. This
way, when I access <code>myservice.example.com</code>, the request is routed through Tailscale to my Kubernetes cluster. Get the
Tailscale DNS name of your Kubernetes cluster from the Tailscale Admin Console under Machines. Open the entry for your
service and search for <strong><code>Full domain</code></strong>. Copy the value and create the CNAME record in your DNS provider:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>CNAME myservice.example.com -&gt; &lt;tailscale-dns-name-of-k8s-cluster&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span># Example:
</span></span><span style="display:flex;"><span>CNAME myservice.example.com -&gt; my-service.tail12345.ts.net
</span></span></code></pre></td></tr></table>
</div>
</div><p>Consider that DNS propagation might take some time depending on your DNS provider. You can use tools like dig to check
if the DNS record has propagated:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dig +short myservice.example.com
</span></span></code></pre></td></tr></table>
</div>
</div><p>And that&rsquo;s it! Now I can access my Kubernetes applications securely through Tailscale.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this blog post, I explained how I set up Tailscale in my Kubernetes cluster using the Tailscale Operator and how I
access my Kubernetes applications through Tailscale using the Tailscale Ingress Controller. Tailscale provides a secure
and private way to connect to services without exposing them to the public internet. I hope this guide helps you set up
Tailscale in your own Kubernetes cluster! Thanks to Josh Noll for his great guide on this topic.</p>
<h2 id="additional-resources">Additional resources</h2>
<ul>
<li><a href="https://joshrnoll.com/securely-exposing-applications-on-kubernetes-with-tailscale/#using-the-tailscale-ingress-controller">Josh Nolls&rsquo; Guide</a></li>
</ul>
]]></content:encoded></item><item><title>How to set up a Tailscale account with OIDC?</title><link>/post/how-to-set-up-tailscale-with-zitadel/</link><pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate><guid>/post/how-to-set-up-tailscale-with-zitadel/</guid><description>&lt;h2 id="why-i-want-to-use-tailscale"&gt;Why I want to use Tailscale&lt;/h2&gt;
&lt;p&gt;Tailscale is a VPN service that makes it easy to create secure networks and connections between devices. The main reason
I want to use Tailscale is that I need a secure and private way to access services that should not be publicly
accessible. Tailscale allows me to create a virtual private network (VPN) where my devices and services can communicate
securely over the internet, without exposing them to the public.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="why-i-want-to-use-tailscale">Why I want to use Tailscale</h2>
<p>Tailscale is a VPN service that makes it easy to create secure networks and connections between devices. The main reason
I want to use Tailscale is that I need a secure and private way to access services that should not be publicly
accessible. Tailscale allows me to create a virtual private network (VPN) where my devices and services can communicate
securely over the internet, without exposing them to the public.</p>
<h2 id="why-i-want-to-use-zitadel-as-an-oidc-provider">Why I want to use ZITADEL as an OIDC provider</h2>
<p>ZITADEL is an IdP that provides identity and access management. It is already my main IdP for other services, so it
makes sense to use it for Tailscale as well. By using ZITADEL as my OIDC provider, I can reuse my existing user accounts
and authentication mechanisms, which simplifies the overall management of my Tailscale network.</p>
<h2 id="why-i-created-this-guide">Why I created this guide</h2>
<p>Even though setting up Tailscale with an OIDC provider is quite well documented, I still faced some difficulties during
the configuration process. Therefore, I decided to create this guide to help others who might face similar challenges
when setting up Tailscale with ZITADEL as their OIDC provider.</p>
<p>First, some background on how this works: Tailscale loads the OIDC configuration using WebFinger, based on the domain of
the email address you use to log in. For example, if I want to use my own email account <code>name@example.com</code> to
authenticate with Tailscale, Tailscale will look for the OIDC configuration at:</p>
<p><code>https://example.com/.well-known/webfinger</code></p>
<p>This endpoint must return the following JSON:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;subject&#34;</span>: <span style="color:#a5d6ff">&#34;acct:name@example.com&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;links&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#7ee787">&#34;rel&#34;</span>: <span style="color:#a5d6ff">&#34;http://openid.net/specs/connect/1.0/issuer&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#7ee787">&#34;href&#34;</span>: <span style="color:#a5d6ff">&#34;https://account.example.com&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>href</code> field points to the issuer URL of the OIDC provider. You can find this value in the OpenID configuration
endpoint:</p>
<p><code>https://account.example.com/.well-known/openid-configuration</code></p>
<p>Also note that the <code>rel</code> field is a fixed value defined by the OpenID Connect specification.</p>
<h2 id="using-the-webfinger-of-my-main-domain">Using the WebFinger of my main domain</h2>
<p>Since I already have a website running at <code>example.com</code>, but did not want to add the WebFinger file to my main website,
I tried to set up a dedicated WebFinger server. However, there are only a few WebFinger server implementations
available, and none of them worked well for my use case. Most of them are no longer actively maintained, or cannot
easily be deployed on Kubernetes.</p>
<p>For example, I tried <a href="https://github.com/peeley/carpal">https://github.com/peeley/carpal</a>, which seemed to be one of the
more popular and maintained projects. However, it defines the WebFinger subject (such as <code>acct:name@example.com</code>)
directly in the filename. Kubernetes does not allow mounting files with special characters like <code>:</code> and <code>@</code>, which made
this approach unusable for me. I also did not want to introduce an external database just for running a WebFinger
server.</p>
<p>In the end, I decided to use a static Caddy server and simply serve a static JSON file.</p>
<h3 id="caddyfile---configmap">Caddyfile - ConfigMap</h3>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ConfigMap</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy-config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">data</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">Caddyfile</span>:<span style="color:#6e7681"> </span>|<span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">    :{{ .Values.service.port | default 8080 }} {
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      root * /srv
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      try_files {path} /.well-known/webfinger
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      file_server
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      header {
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">        Content-Type application/jrd+json
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      }
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">    }</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="webfingerjson---configmap">webfinger.json - ConfigMap</h3>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ConfigMap</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-resource</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">data</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">webfinger.json</span>:<span style="color:#6e7681"> </span>|<span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">    {
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      &#34;subject&#34;: &#34;acct:name@example.com&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      &#34;links&#34;: [
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">        {
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">          &#34;rel&#34;: &#34;http://openid.net/specs/connect/1.0/issuer&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">          &#34;href&#34;: &#34;https://account.example.com&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">        }
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">      ]
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">    }</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="deployment">Deployment</h3>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">45
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">apps/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Deployment</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">replicas</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">selector</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">matchLabels</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">template</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">labels</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">containers</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy:alpine</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">imagePullPolicy</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">IfNotPresent</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>- <span style="color:#7ee787">containerPort</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">resources</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">requests</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">cpu</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">15m</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">memory</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">10Mi</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">limits</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">memory</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">16Mi</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">volumeMounts</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#8b949e;font-style:italic"># Caddy config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy-config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">mountPath</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">/etc/caddy/Caddyfile</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">subPath</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Caddyfile</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#8b949e;font-style:italic"># WebFinger JSON served as a FILE</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-resource</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">mountPath</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">/srv/.well-known/webfinger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">subPath</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger.json</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">volumes</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy-config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">configMap</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">caddy-config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-resource</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">configMap</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-resource</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="service">Service</h3>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">selector</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">http</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">port</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">targetPort</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="ingress">Ingress</h3>
<p>You can keep your other ingress for the main website and just add a new path for the webfinger service. However, both
need to be accessible on the same host.</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">28
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">networking.k8s.io/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Ingress</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;webfinger-path&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">annotations</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">nginx.ingress.kubernetes.io/rewrite-target</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">/</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">nginx.ingress.kubernetes.io/use-regex</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;true&#34;</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># required to mach the guest path</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">kubernetes.io/ingress.class</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;nginx&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">cert-manager.io/cluster-issuer</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;letsencrypt-prod&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">nginx.ingress.kubernetes.io/backend-protocol</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">HTTP</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">nginx.ingress.kubernetes.io/force-ssl-redirect</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;true&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">nginx.ingress.kubernetes.io/ssl-redirect</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;true&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">rules</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">host</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">example.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">http</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">paths</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span>- <span style="color:#7ee787">path</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">/.well-known</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">pathType</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ImplementationSpecific</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">backend</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">              </span><span style="color:#7ee787">service</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span><span style="color:#7ee787">port</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                  </span><span style="color:#7ee787">number</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">tls</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">hosts</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#a5d6ff">example.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">secretName</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">webfinger-service-tls</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="configure-zitadel">Configure ZITADEL</h2>
<p>Create a new ZITADEL application for Tailscale and make sure that the user you want to use for login has access to this
application. It is important that the email address or username matches the <code>subject</code> you configured in the WebFinger
setup.</p>
<p>Create a <strong>Web Application</strong> of type <code>Code</code> and add the following Redirect URI:</p>
<p><code>https://login.tailscale.com/a/oauth_response</code></p>
<h2 id="configure-tailscale">Configure Tailscale</h2>
<p>In Tailscale, go to the sign-up page and choose <code>OIDC</code> as the authentication method. Enter the email address associated
with your ZITADEL account (for example, <code>name@example.com</code>). Tailscale will use WebFinger to discover the OIDC provider
and then redirect you to ZITADEL for authentication.</p>
<p>After a successful login, you will be prompted to enter the <code>clientId</code> and <code>clientSecret</code> of the ZITADEL application you
created earlier. Once these credentials are entered, you should be redirected back to Tailscale and logged in
successfully.</p>
<h2 id="how-to-verify-that-everything-works">How to verify that everything works</h2>
<p>To verify that everything is working correctly, you can open an incognito or private browsing window in your web browser
and try to log in to Tailscale using the same email address.</p>
<p>Keep in mind that you must first enter your email address on the Tailscale sign-in page. After that, you will be
redirected to ZITADEL for authentication.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Setting up Tailscale with ZITADEL as a OIDC provider needs some tweaks, especially when it comes to the WebFinger
server. However, this only lets you create a basic Tailscale account. If you want to protect your services with
Tailscale, you will need to add Tailscale in front of your services, which is a topic for another guide.</p>
<h2 id="additional-resources">Additional resources</h2>
<ul>
<li><a href="https://tailscale.com/kb/1240/sso-custom-oidc">Tailscale OIDC documentation</a></li>
</ul>
]]></content:encoded></item><item><title>Protecting Harbor with Zitadel</title><link>/post/protect-harbor-with-zitadel/</link><pubDate>Sat, 30 Aug 2025 00:00:00 +0000</pubDate><guid>/post/protect-harbor-with-zitadel/</guid><description>How I protected my Harbor instance with Zitadel.</description><content:encoded><![CDATA[<h2 id="what-is-harbor">What is Harbor?</h2>
<p>Harbor is an open-source container image registry that can also function as a chart or image proxy. This is especially
useful when you want to cache images from Docker Hub or other registries. My main reason for using Harbor was to bypass
the rate limits imposed by Docker Hub.</p>
<h2 id="what-is-zitadel">What is Zitadel?</h2>
<p>Zitadel is an open-source identity and access management (IAM) solution that provides authentication and authorization
services. It supports multiple authentication methods, including OAuth2, OpenID Connect and so on. Zitadel allows
you to manage user identities, roles, and permissions for your applications and services.</p>
<h2 id="how-to-protect-harbor-with-zitadel">How to Protect Harbor with Zitadel</h2>
<p>To secure Harbor with Zitadel, you need to configure Harbor to use Zitadel as an external authentication provider. My
goal was to achieve this by updating the Harbor configuration file and setting up Zitadel as an OAuth2 provider.
However, a few additional steps were required to make it work.</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>
<p>A running Harbor instance (I used the <a href="https://github.com/goharbor/harbor-helm">official Helm chart</a>)</p>
<ul>
<li>If you want to use PKCE, make sure to use Harbor v2.13.0 or higher.</li>
</ul>
</li>
<li>
<p>A running Zitadel instance (I used the <a href="https://github.com/zitadel/zitadel-charts">official Helm chart</a>).</p>
</li>
</ul>
<h3 id="step-1-configure-zitadel">Step 1: Configure Zitadel</h3>
<ol>
<li>
<p>Log in to your Zitadel instance and create a new PKCE application.</p>
<ul>
<li>The redirect URL should be <code>https://&lt;your-harbor-domain&gt;/c/oidc/callback</code>.</li>
</ul>
</li>
<li>
<p>In the <code>Token Settings</code> tab, enable <code>User roles inside ID Token</code>.</p>
</li>
<li>
<p>Create a role called <code>harbor_administrators</code> (or any name you prefer) and assign it to users who should have admin
access in Harbor.</p>
</li>
<li>
<p>Go to the <code>Action</code> tab and create a new action:</p>
<ul>
<li>
<p>Name: <code>flatRoles</code> <strong>(the name must match the function name)</strong></p>
</li>
<li>
<p>Code:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">function</span> flatRoles(ctx, api) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (ctx.v1.user.grants <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#79c0ff">undefined</span> <span style="color:#ff7b72;font-weight:bold">||</span> ctx.v1.user.grants.count <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">0</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">return</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> grants <span style="color:#ff7b72;font-weight:bold">=</span> [];
</span></span><span style="display:flex;"><span>    ctx.v1.user.grants.grants.forEach(claim =&gt; {
</span></span><span style="display:flex;"><span>        claim.roles.forEach(role =&gt; {
</span></span><span style="display:flex;"><span>            grants.push(role)  
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    api.v1.claims.setClaim(<span style="color:#a5d6ff">&#39;groups&#39;</span>, grants)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>Timeout: <code>10</code> seconds</p>
</li>
<li>
<p>Enable <code>Allowed To Fail</code></p>
</li>
</ul>
</li>
<li>
<p>Go to the <code>Flow</code> section and select <code>Complement Token</code> as the <code>Flow Type</code>.</p>
</li>
<li>
<p>Add triggers: <code>Pre Userinfo creation</code> and <code>Pre access token creation</code>, selecting the <code>flatRoles</code> action for both
triggers.</p>
</li>
<li>
<p>Update your Harbor configuration with the following <code>userSettings</code>:</p>
<blockquote>
<p>Make sure the JSON is valid. If you get errors, check for issues like comments (<code>//</code>) which aren’t valid in JSON.</p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;auth_mode&#34;</span>: <span style="color:#a5d6ff">&#34;oidc_auth&#34;</span>, <span style="color:#8b949e;font-style:italic">// must be set to oidc_auth to enable OIDC authentication
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_name&#34;</span>: <span style="color:#a5d6ff">&#34;Zitadel&#34;</span>, <span style="color:#8b949e;font-style:italic">// display name for the OIDC provider
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_endpoint&#34;</span>: <span style="color:#a5d6ff">&#34;https://zitadel.domain.com&#34;</span>, <span style="color:#8b949e;font-style:italic">// issuer of your Zitadel instance
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_client_id&#34;</span>: <span style="color:#a5d6ff">&#34;335927275759403852&#34;</span>, <span style="color:#8b949e;font-style:italic">// client ID of your PKCE application we created earlier
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_scope&#34;</span>: <span style="color:#a5d6ff">&#34;openid,profile,email,offline_access&#34;</span>, <span style="color:#8b949e;font-style:italic">// scopes to request
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_groups_claim&#34;</span>: <span style="color:#a5d6ff">&#34;groups&#34;</span>, <span style="color:#8b949e;font-style:italic">// claim in the ID token that contains the user&#39;s groups
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>  <span style="color:#7ee787">&#34;oidc_admin_group&#34;</span>: <span style="color:#a5d6ff">&#34;harbor_administrators&#34;</span> <span style="color:#8b949e;font-style:italic">// group (which we created earlier) that will be mapped to Harbor administrators
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>Restart Harbor to apply the changes.</p>
</li>
<li>
<p>Log out of Harbor; you should see a new login button with the name specified in <code>oidc_name</code>.</p>
</li>
<li>
<p>Log in with a user assigned to the <code>harbor_administrators</code> role to gain admin access.</p>
</li>
</ol>
<p>That&rsquo;s it! Your Harbor instance is now using Zitadel as IdP, allowing you to manage user access and permissions
centrally.</p>
<hr>
<h2 id="troubleshooting">Troubleshooting</h2>
<p>To inspect your JWT token, you can use <a href="https://auth-playground.makefermion.com/oauth2/">Auth-Playground</a>. To check the
group claim, temporarily add <code>https://auth-playground.makefermion.com/oauth2/</code> as a redirect URL in your PKCE
application on Zitadel.</p>
<p><strong>Settings example:</strong></p>
<ul>
<li>Authorize URL: <code>https://zitadel.domain.com/oauth/v2/authorize</code></li>
<li>Redirect URL: <code>https://auth-playground.makefermion.com/oauth2</code></li>
<li>Client-ID: <code>&lt;your-client-id-of-the-pkce-application&gt;</code></li>
<li>Client Secret: leave empty (not needed for PKCE)</li>
<li>Response type: <code>code</code></li>
<li>Code Challenge: <code>&lt;your-code-challange&gt;</code> (ignore the placeholder on the site)</li>
<li>Code Challenge Method: <code>S256</code></li>
<li>Scopes: <code>openid profile email offline_access</code></li>
<li>Custom attributes: <code>state=&lt;random-string&gt;</code></li>
</ul>
<p>After logging in, you’ll be redirected back to Auth-Playground. Copy the code shown in the pop-up and exchange it for a
token:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl --request POST <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --url <span style="color:#a5d6ff">&#39;https://zitadel.domain.com/oauth/v2/token&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --header <span style="color:#a5d6ff">&#39;Content-Type: application/x-www-form-urlencoded&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --data-urlencode <span style="color:#a5d6ff">&#39;client_id=&lt;your-client-id&gt;&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --data-urlencode <span style="color:#a5d6ff">&#39;code_verifier=&lt;original-code-verifier&gt;&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --data-urlencode <span style="color:#a5d6ff">&#39;code=&lt;code-from-popup&gt;&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --data-urlencode <span style="color:#a5d6ff">&#39;grant_type=authorization_code&#39;</span> <span style="color:#79c0ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#79c0ff"></span>  --data-urlencode <span style="color:#a5d6ff">&#39;redirect_uri=https://auth-playground.makefermion.com/oauth2&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="generate-code-challenge-and-state">Generate Code Challenge and State</h3>
<p>Generate a code verifier and challenge with:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#79c0ff">CODE_VERIFIER</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#ff7b72">$(</span>openssl rand -base64 <span style="color:#a5d6ff">64</span> | tr -d <span style="color:#a5d6ff">&#39;=+/&#39;</span> | tr -d <span style="color:#a5d6ff">&#39;\n&#39;</span><span style="color:#ff7b72">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#a5d6ff">&#34;CODE_VERIFIER: </span><span style="color:#79c0ff">$CODE_VERIFIER</span><span style="color:#a5d6ff">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">CODE_CHALLENGE</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#ff7b72">$(</span>echo -n <span style="color:#79c0ff">$CODE_VERIFIER</span> | openssl dgst -binary -sha256 | openssl base64 | tr -d <span style="color:#a5d6ff">&#39;=+/&#39;</span><span style="color:#ff7b72">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#a5d6ff">&#34;CODE_CHALLENGE: </span><span style="color:#79c0ff">$CODE_CHALLENGE</span><span style="color:#a5d6ff">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#79c0ff">STATE</span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#ff7b72">$(</span>openssl rand -base64 <span style="color:#a5d6ff">32</span> | tr -d <span style="color:#a5d6ff">&#39;=+/&#39;</span> | tr -d <span style="color:#a5d6ff">&#39;\n&#39;</span><span style="color:#ff7b72">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#a5d6ff">&#34;STATE: </span><span style="color:#79c0ff">$STATE</span><span style="color:#a5d6ff">&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><hr>
<h2 id="references">References</h2>
<ul>
<li><a href="https://goharbor.io/docs/2.5.0/install-config/configure-user-settings-cli/">Harbor - Configure User Settings</a></li>
<li><a href="https://github.com/netbirdio/netbird/issues/1713#issuecomment-2248599276">Helpful comment on Zitadel Action</a></li>
</ul>
]]></content:encoded></item><item><title>Protect services with Zitadel and OAuth2-Proxy</title><link>/post/protect-services-with-zitadel/</link><pubDate>Thu, 31 Jul 2025 00:00:00 +0000</pubDate><guid>/post/protect-services-with-zitadel/</guid><description>Protect services with Zitadel and OAuth2-Proxy</description><content:encoded><![CDATA[<p>I have several services deployed and exposed on my Kubernetes cluster. Some of them need to be publicly accessible,
while others should remain private. On top of that, I don’t fully trust the built-in security of some apps, so I decided
to add an extra layer of protection with <a href="https://github.com/oauth2-proxy/oauth2-proxy">OAuth2-Proxy</a>.</p>
<hr>
<h2 id="setup">Setup</h2>
<p>I already had a Zitadel instance running, along with an Ingress controller and Cert-Manager.</p>
<p>The first service I wanted to secure was the Kubernetes Dashboard. I deployed OAuth2-Proxy and configured it. At first,
I mistakenly set up an internal upstream. This worked fine for the dashboard itself, but not for other apps - since
OAuth2-Proxy only supports a single upstream.</p>
<p>That’s when I realized I had configured OAuth2-Proxy as a reverse proxy, instead of just as an authentication layer. I
removed the upstream setting and instead defined allowed domains.</p>
<p>One important detail: when setting domain values, you must include the leading dot (e.g., <code>.domain.tld</code>). Without it,
subdomains won’t be allowed through OAuth2-Proxy.</p>
<p>Here’s my final configuration:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>provider = <span style="color:#a5d6ff">&#34;oidc&#34;</span>
</span></span><span style="display:flex;"><span>oidc_issuer_url = <span style="color:#a5d6ff">&#34;https://zitadel.domain-a.tld&#34;</span>
</span></span><span style="display:flex;"><span>scope = <span style="color:#a5d6ff">&#34;openid email profile&#34;</span>
</span></span><span style="display:flex;"><span>code_challenge_method = <span style="color:#a5d6ff">&#34;S256&#34;</span> <span style="color:#8b949e;font-style:italic"># Required for OIDC providers that use PKCE</span>
</span></span><span style="display:flex;"><span>pass_access_token = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>skip_provider_button = <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>ssl_insecure_skip_verify = <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>proxy_prefix = <span style="color:#a5d6ff">&#34;/oauth2&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>email_domains = [<span style="color:#a5d6ff">&#34;.domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;.domain-b.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-b.tld&#34;</span>]
</span></span><span style="display:flex;"><span>whitelist_domains = [<span style="color:#a5d6ff">&#34;.domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;.domain-b.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-b.tld&#34;</span>]
</span></span><span style="display:flex;"><span>cookie_domains = [<span style="color:#a5d6ff">&#34;.domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;.domain-b.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-a.tld&#34;</span>, <span style="color:#a5d6ff">&#34;domain-b.tld&#34;</span>]
</span></span><span style="display:flex;"><span>cookie_name = <span style="color:#a5d6ff">&#34;_oauth2_proxy&#34;</span>
</span></span><span style="display:flex;"><span>cookie_expire = <span style="color:#a5d6ff">&#34;168h&#34;</span>
</span></span><span style="display:flex;"><span>cookie_secure = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>cookie_httponly = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>cookie_samesite = <span style="color:#a5d6ff">&#34;lax&#34;</span>
</span></span><span style="display:flex;"><span>cookie_csrf_per_request = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>cookie_csrf_expire = <span style="color:#a5d6ff">&#34;5m&#34;</span>
</span></span><span style="display:flex;"><span>set_xauthrequest = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>pass_user_headers = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>pass_authorization_header = <span style="color:#79c0ff">true</span>
</span></span><span style="display:flex;"><span>pass_basic_auth = <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>reverse_proxy = <span style="color:#79c0ff">false</span>
</span></span><span style="display:flex;"><span>show_debug_on_error = <span style="color:#79c0ff">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You also need to provide the following environment variables. While you could set them directly in the config file, I
prefer to keep them in 1Password and sync them into my namespace as Kubernetes secrets — for better security and easier
management:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">env</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">OAUTH2_PROXY_CLIENT_ID</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">valueFrom</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">secretKeyRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">oauth2-proxy-credentials</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">key</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">client-id</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">OAUTH2_PROXY_CLIENT_SECRET</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">valueFrom</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">secretKeyRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">oauth2-proxy-credentials</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">key</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">client-secret</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">OAUTH2_PROXY_COOKIE_SECRET</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">valueFrom</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">secretKeyRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">oauth2-proxy-credentials</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">key</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">cookie-secret</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><em>Note: If you’re using Let’s Encrypt <strong>staging</strong> certificates, you’ll need to set <code>ssl_insecure_skip_verify = true</code>.
Once you switch to <strong>production</strong> certificates, you can (and should) set it back to <code>false</code>.</em></p>
<hr>
<h2 id="adding-oauth2-proxy-to-ingress">Adding OAuth2-Proxy to Ingress</h2>
<p>Once OAuth2-Proxy was properly configured and I could log in with my Zitadel account, I integrated it with my Ingress.
After a bit of trial and error, I ended up with this setup, which now works reliably across all my services:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">kubernetes.io/ingress.class</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;nginx&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">cert-manager.io/cluster-issuer</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;letsencrypt-prod&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">nginx.ingress.kubernetes.io/force-ssl-redirect</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;true&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">nginx.ingress.kubernetes.io/ssl-redirect</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;true&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">nginx.ingress.kubernetes.io/auth-url</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;https://oauth-proxy.domain.tld/oauth2/auth&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">nginx.ingress.kubernetes.io/auth-signin</span>:<span style="color:#6e7681"> </span>&gt;-<span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">  https://oauth-proxy.domain.tld/oauth2/start?rd=$scheme://$host$request_uri</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Depending on the service, I sometimes add these extra annotations:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">nginx.ingress.kubernetes.io/backend-protocol</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;HTTPS&#34;</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># or &#34;HTTP&#34; if the service doesn’t use TLS</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">nginx.ingress.kubernetes.io/proxy-buffer-size</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;8k&#34;</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This setup is based on
the <a href="https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth">official Ingress NGINX OAuth2-Proxy guide</a>,
with a few tweaks to fit my use case.</p>
]]></content:encoded></item><item><title>Kubernetesize everything</title><link>/post/kubernetesize-everything/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><guid>/post/kubernetesize-everything/</guid><description>&lt;p&gt;&lt;img alt="K8s" loading="lazy" src="../images/kubernetesize-everything/kubernetes-and-infomaniak.png"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In advance, I want to say that I&amp;rsquo;m still quite new to the whole Kubernetes world, so please be understanding if there
are better ways to do things or if I did something wrong. :)&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;In autumn 2024, I heard about a new managed Kubernetes service here in Switzerland by my preferred hosting company,
Infomaniak. The service was not released yet, but I already started planning the migration of my whole infrastructure.
At that time, it was distributed across several VPSs, a Kubernetes cluster at DigitalOcean, and some services running at
home. I started searching for Helm charts or wrote my own if none were available.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img alt="K8s" loading="lazy" src="/images/kubernetesize-everything/kubernetes-and-infomaniak.png"></p>
<blockquote>
<p>In advance, I want to say that I&rsquo;m still quite new to the whole Kubernetes world, so please be understanding if there
are better ways to do things or if I did something wrong. :)</p></blockquote>
<p>In autumn 2024, I heard about a new managed Kubernetes service here in Switzerland by my preferred hosting company,
Infomaniak. The service was not released yet, but I already started planning the migration of my whole infrastructure.
At that time, it was distributed across several VPSs, a Kubernetes cluster at DigitalOcean, and some services running at
home. I started searching for Helm charts or wrote my own if none were available.</p>
<p>After Infomaniak finally released their managed Kubernetes service, I could breathe a sigh of relief, since the pricing
was more or less what I expected. My main fear was that it would be as expensive as other providers and therefore not
really worth it for a private person to run a cluster. Fortunately, Infomaniak released a free control plane, and you
only pay for the worker nodes via their public cloud pricing.</p>
<p>I opened their calculator and added two worker nodes with 4 CPUs and 8 GB of memory each. The price was reasonable, and I
could easily upgrade later to the next node size with 4 CPUs and 16 GB of memory. I also added a Load Balancer and a
public IPv4 address to my shopping cart and started setting up my new Kubernetes cluster.</p>
<ul>
<li><a href="https://www.infomaniak.com/de/hosting/public-cloud/kubernetes">Infomaniak&rsquo;s Kubernetes service</a></li>
<li><a href="https://www.infomaniak.com/de/hosting/public-cloud">Infomaniak&rsquo;s Public Cloud</a></li>
</ul>
<h2 id="security-notes">Security Notes</h2>
<blockquote>
<p>At this point, it is important to mention that this article describes a personal Kubernetes setup and intentionally
omits some production-grade hardening steps.</p></blockquote>
<p>⚠️ Important considerations:</p>
<ul>
<li>The Kubernetes Dashboard should never be exposed publicly.</li>
<li><code>cluster-admin</code> roles are used here for demonstration purposes only.</li>
<li>Kubernetes Secrets are base64-encoded, not encrypted.</li>
<li>All credentials shown are placeholders.</li>
<li>Real secrets are managed via the 1Password Kubernetes Operator.</li>
</ul>
<p>If you plan to use similar patterns in production, additional measures such as RBAC minimization, NetworkPolicies,
secret encryption at rest, and private cluster access are strongly recommended.</p>
<h2 id="services">Services</h2>
<p>As mentioned earlier, I had several different VPSs and other pieces of infrastructure. For example, I was running
<a href="https://github.com/InvoicePlane/InvoicePlane">InvoicePlane</a>, <a href="https://github.com/shlinkio/shlink">Shlink</a>, and more
(services to be added later). My goal was to migrate all of them to the new Kubernetes cluster.</p>
<p>For services like Shlink, where the UI is not protected by default, I wanted to add an authentication layer in front of
them. Because of that, I decided to use Zitadel. I had already used it in other projects and was always very satisfied
with it.</p>
<p>I then started writing my own Helm charts where no official or community-maintained ones existed. The first service I
deployed on the cluster was the NGINX Ingress Controller, which was quite simple (see the example below). After that, I
installed cert-manager, which was also easy to deploy. Then I created a custom resource to define two <code>ClusterIssuer</code>s
for issuing TLS certificates.</p>
<p><strong><code>values.yaml</code> for the ingress-nginx Helm chart</strong></p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">controller</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">admissionWebhooks</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">enabled</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">patch</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">enabled</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">service</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">externalTrafficPolicy</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Local</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><strong><code>values.yaml</code> for the cert-manager Helm chart</strong></p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">cert-manager</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">enabled</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">crds</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">enabled</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">issuer</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">email</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">hi@mydomain.com</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><strong>ClusterIssuer</strong></p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">29
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">cert-manager.io/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ClusterIssuer</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">letsencrypt-staging</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">acme</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">server</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">https://acme-staging-v02.api.letsencrypt.org/directory</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">email</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">hi@mydomain.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">privateKeySecretRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">letsencrypt-staging</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">solvers</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#7ee787">http01</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">ingress</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">ingressClassName</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">nginx</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#ff7b72">---</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">cert-manager.io/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ClusterIssuer</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">letsencrypt-prod</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">acme</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">server</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">https://acme-v02.api.letsencrypt.org/directory</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">email</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">hi@mydomain.com</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">privateKeySecretRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">letsencrypt-prod</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">solvers</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#7ee787">http01</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">ingress</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span><span style="color:#7ee787">ingressClassName</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">nginx</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>After having those two issuers in place, I wanted to deploy the Kubernetes Dashboard. Since I had already deployed it on
my old DigitalOcean cluster, I remembered it as a single pod (or at least it was back then). This time, however, I ended
up with several pods, proxies, and additional components.</p>
<p>I initially tried to simplify the deployment because I wanted to keep it as minimal as possible. After spending several
hours on this, I decided to use the default setup instead—which, in most cases, is the better approach anyway. And who
would have thought: it worked perfectly.</p>
<p>I opened my browser, navigated to the Kubernetes Dashboard, typed <code>thisisunsafe</code> to bypass Chrome’s SSL warning, and
there it was—my dashboard. I then noticed that I could no longer log in using my kubeconfig, as I was able to do with
the old dashboard. Instead, I created a service account and issued a token for authentication.</p>
<p>Typing <code>thisisunsafe</code> should only be done on trusted networks, as it bypasses important security warnings. In a
production environment, you should always issue a valid TLS certificate for the dashboard to avoid these warnings
entirely.</p>
<p>Apply the following YAML to create a service account with the <code>cluster-admin</code> role:</p>
<blockquote>
<p><strong>Warning</strong>: Granting <code>cluster-admin</code> permissions to a service account gives it full access to the entire cluster.
Make sure you understand the security implications before proceeding. In production environments, more restrictive
roles are strongly recommended.</p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ServiceAccount</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">kube-ds-admin</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">namespace</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">kube-system</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#ff7b72">---</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">rbac.authorization.k8s.io/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ClusterRoleBinding</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">kube-ds-admin-role-binding</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">subjects</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>- <span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ServiceAccount</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">kube-ds-admin</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">namespace</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">kube-system</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">roleRef</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">ClusterRole</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">cluster-admin</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">apiGroup</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">rbac.authorization.k8s.io</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Then generate a token for the service account:</p>
<blockquote>
<p><strong>Warning</strong>: The token generated below is valid for 1 hour. Adjust the <code>--duration</code> flag as needed for your use case.
Avoid long-lived tokens in production environments due to increased security risks.</p>
<p>If you generate a long-lived token and want to revoke it later, you can do so by deleting the service account or the
associated secret.</p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl create token kube-ds-admin -n kube-system --duration<span style="color:#ff7b72;font-weight:bold">=</span>1h
</span></span></code></pre></td></tr></table>
</div>
</div><p>Some time ago, I noticed that 1Password provides its own Kubernetes operator, which allows syncing specific 1Password
items directly into a Kubernetes cluster. My curiosity and the security benefits convinced me to try it out.</p>
<p>During the setup, I ran into a very annoying issue. Everything seemed to be configured correctly, but the secret simply
would not sync. After spending much more time on this than I want to admit, I finally realized that I had missed setting
the <code>operator.token.value</code> in the Helm command. I had only attached the <code>credentials.json</code> file. Even though this is
clearly documented in the 1Password documentation, I still managed to miss it.</p>
<p>Once this was fixed, everything worked as expected. I was able to sync credentials into Kubernetes. I created a dummy
application that displayed the synced secret, and when I changed the credential in 1Password, it was automatically
synced to Kubernetes and the pod restarted.</p>
<p>Before discovering the 1Password operator, my plan was to encrypt secrets using Sealed Secrets. That way, I could store
the encrypted files directly in my repository.</p>
<p>Next, I had to figure out how to deploy probably the most important services of all: databases. Unfortunately, I need
several of them—MySQL, PostgreSQL, and MongoDB.</p>
<p>I decided to start with PostgreSQL because I need it for Zitadel. My first choice was the Zalando Postgres Operator, but
after installing it, I ran into issues with backups and was generally not convinced by its behavior. I therefore
decided to move on and try the CloudNativePG (CNPG) operator.</p>
<p>At first, I was very happy with it—it worked well and felt clean. However, later on, I experienced some issues with the
<code>initDB</code> and backup functionality. My main problem with <code>initDB</code> was that I did not really want it. I prefer to store
the database custom resource inside the service directory. For example, the database for Zitadel should live in the
Zitadel folder.</p>
<p>The reason for this is that I want to deploy everything using ArgoCD in the future. In my opinion, it makes the most
sense to keep related resources in one directory. Unfortunately, I did not find a way to fully disable the <code>initDB</code>
behavior.</p>
<p>At some point, I decided that it might be acceptable to leave the default behavior in place. Still, it felt a bit
unclear to generate a secret and just leave it there without a proper backup—especially since the database would
probably stay empty forever.</p>
<p>As a workaround, I added a 1Password custom resource to the database directory and synced the default <code>initDB</code>
credentials from 1Password.</p>
<blockquote>
<p><strong>Note</strong>: Do not store secrets in your Git repository in plain text. Since the values are only base64-encoded, they
can be easily decoded. Use a secret management solution like 1Password, HashiCorp Vault, or similar tools to manage
secrets securely. Another option is to use Sealed Secrets to encrypt secrets before committing them to your
repository.</p></blockquote>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># This is only the generated secret format expected by CNPG.</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#8b949e;font-style:italic"># In my setup, the secret is synced from 1Password to Kubernetes.</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Secret</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">postgres-initdb-credentials</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">namespace</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">my-database-cluster</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681"></span><span style="color:#7ee787">data</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">username: bXktZGItdXNlcm5hbWU= # base64-encoded</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">`my-db-username`</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">password: bXktc3VwZXItc2VjdXJlLWRiLXBhc3N3b3k= # base64-encoded</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">`my-super-secure-db-password`</span><span style="color:#6e7681">
</span></span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>write about MongoDB and MySQL</li>
<li>generalize and summarize the service deployment</li>
<li>details about Zitadel will be covered in a separate article</li>
</ul>
]]></content:encoded></item><item><title>Pingvin Share on Kubernetes</title><link>/post/pingvin-share/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><guid>/post/pingvin-share/</guid><description>&lt;blockquote&gt;
&lt;p&gt;Unfortunately, Pingvin Share is no longer maintained. Because of that, I’ve stopped using it and am now exploring
other tools.
&lt;a href="https://github.com/stonith404/pingvin-share/issues/857#issue-3186197261"&gt;Read post&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I often find myself in situations where I want to share files with others, but I want something simpler than services
like Google Drive or kDrive. So, I spent quite a bit of time looking for a solution and eventually
discovered &lt;a href="https://github.com/stonith404/pingvin-share"&gt;Pingvin Share&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wanted to run it on my Kubernetes cluster, so I made my first &amp;ldquo;bigger&amp;rdquo; open-source contribution by adding S3 support.
A bit later, I also added the option to configure it using a file. After that, I took a break for a while, since I had a
lot of other things going on.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
<p>Unfortunately, Pingvin Share is no longer maintained. Because of that, I’ve stopped using it and am now exploring
other tools.
<a href="https://github.com/stonith404/pingvin-share/issues/857#issue-3186197261">Read post</a></p></blockquote>
<p>I often find myself in situations where I want to share files with others, but I want something simpler than services
like Google Drive or kDrive. So, I spent quite a bit of time looking for a solution and eventually
discovered <a href="https://github.com/stonith404/pingvin-share">Pingvin Share</a>.</p>
<p>I wanted to run it on my Kubernetes cluster, so I made my first &ldquo;bigger&rdquo; open-source contribution by adding S3 support.
A bit later, I also added the option to configure it using a file. After that, I took a break for a while, since I had a
lot of other things going on.</p>
<p>When I wanted to continue working on making Pingvin Share more Kubernetes-friendly, I saw that someone else had already
added a key feature: Postgres support—since it had only used SQLite before. With that, all the pieces were in place to
deploy it on Kubernetes. Unfortunately, the project lost its maintenance before this merge request was merged.</p>
]]></content:encoded></item><item><title>My Kubernetes setup</title><link>/post/my-kubernetes-setup/</link><pubDate>Tue, 29 Jul 2025 00:00:00 +0000</pubDate><guid>/post/my-kubernetes-setup/</guid><description>My current Kubernetes setup and the services I run on it</description><content:encoded><![CDATA[<p>In this post, I’ll show you which services I currently run on my Kubernetes cluster. (This list might not be complete,
as I sometimes deploy new tools without updating the post.) But here’s the current setup.</p>
<hr>
<h2 id="services">Services</h2>
<h3 id="shlink">Shlink</h3>
<p>I use Shlink as a URL shortener. Occasionally, I need to share long links, and shortening them makes things easier. The
Web UI is also protected by OAuth2-Proxy.</p>
<h3 id="zitadel">Zitadel</h3>
<p>This is my preferred identity provider. I use it both for SaaS solutions and to secure my own services. It’s also the
provider used for OAuth2-Proxy authentication.</p>
<h3 id="my-website">My Website</h3>
<p>Just a simple Nuxt-based website. You can visit it at <a href="https://mattiamueggler.ch">mattiamueggler.ch</a>.</p>
<h3 id="my-blog">My Blog</h3>
<p>A small blog where I occasionally write posts about development, DevOps, or other tech topics.</p>
<h3 id="my-saas-solutions">My SaaS Solutions</h3>
<p>Some applications I’ve built are also deployed on my Kubernetes cluster. These were actually the main reason I started
setting up the cluster in the first place.</p>
<h3 id="homer-dashboards">Homer Dashboards</h3>
<p>Homer provides a dashboard that&rsquo;s configured via YAML. I use it to list all my services with quick links. I’ve also
built a custom image so I can include my own SVG icons.</p>
<h3 id="wordpress-site">WordPress Site</h3>
<p>A small WordPress website I host for my sister.</p>
<h3 id="uptime-kuma">Uptime-Kuma</h3>
<p>Uptime-Kuma helps me monitor the availability of my services. I’ve added all my services to it and created dashboards to
quickly check their status.</p>
<h3 id="fluent-bit">Fluent Bit</h3>
<p>After one of my nodes crashed due to disk pressure, I looked for a better logging solution. I decided to use Fluent Bit
to send logs to an S3 bucket and clean them up from the nodes to save space. I also added two new nodes with more disk
capacity.</p>
<h3 id="kube-prometheus-stack">Kube-Prometheus Stack</h3>
<p>To monitor not just the services, but also the nodes and the cluster itself, I set up the kube-prometheus-stack and
added some dashboards. All services are only available internally within the cluster, and the Grafana dashboard is
protected by OAuth2-Proxy.</p>
<h3 id="databases">Databases</h3>
<p>Since many services require a database, I’ve deployed several kinds—MongoDB, PostgreSQL, and MariaDB. To save resources,
I run each database as part of a shared cluster, rather than setting up one per service. I isolate access by creating
individual users for each database.</p>
<p>The only exception is MariaDB: since I can’t easily add more databases via CRs, I created my own Helm chart on top of
the operator. This allows me to manage everything as dependencies, with the correct configuration out of the box.</p>
<h3 id="additional-services">Additional Services</h3>
<p>I also run a few additional services that I only need occasionally.</p>
<hr>
<h2 id="additional-infrastructure">Additional Infrastructure</h2>
<h3 id="kubernetes-dashboard">Kubernetes Dashboard</h3>
<p>To interact with my cluster, I’ve installed the Kubernetes Dashboard and exposed it behind OAuth2-Proxy.</p>
<h3 id="oauth2-proxy">OAuth2-Proxy</h3>
<p>This is a key part of my security setup. Any service that doesn’t need to be publicly accessible is protected with
OAuth2-Proxy. This gives me secure access to internal tools. I might replace it with Tailscale at some point, since that
would be even more secure.</p>
<h3 id="nginx-ingress-controller">NGINX Ingress Controller</h3>
<p>This is the ingress controller I use to expose my services.</p>
<h3 id="cert-manager">Cert Manager</h3>
<p>I use Cert Manager so I don’t have to manually manage TLS certificates. I’ve set up two cluster issuers with Let&rsquo;s
Encrypt—one for staging and one for production—because Let&rsquo;s Encrypt limits the number of requests to the production
API.</p>
<h3 id="argocd">ArgoCD</h3>
<p>I use ArgoCD for GitOps. All my services are defined in a Git repository, and ArgoCD ensures that the cluster state is
always in sync with the repository.</p>
<h3 id="1password-operator">1Password Operator</h3>
<p>This operator is great for syncing secrets, credentials, and passwords directly to my cluster. That way, I don’t have to
encrypt them (e.g., with Sealed Secrets) to store them in GitHub—they’re never stored there at all.</p>
<h3 id="cnpg-operator">CNPG Operator</h3>
<p>I use this operator to manage PostgreSQL instances, including user and database provisioning.</p>
<h3 id="mariadb-operator">MariaDB Operator</h3>
<p>This one handles my MariaDB setup, including backups.</p>
<h3 id="mongodb-operator">MongoDB Operator</h3>
<p>Used to deploy and manage MongoDB.</p>
]]></content:encoded></item><item><title>Hello World</title><link>/post/hello-world/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>/post/hello-world/</guid><description>This is my first post on this blog. It is just a sample post to show how the blog works.</description><content:encoded>&lt;p>This is my first post on this blog. It is just a sample post to show how the blog works.&lt;/p>
</content:encoded></item></channel></rss>