<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on elijah's blog</title><link>https://blog.cheesedipper.com/post/</link><description>Recent content in Posts on elijah's blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sat, 18 Apr 2026 14:28:11 -0400</lastBuildDate><atom:link href="https://blog.cheesedipper.com/post/index.xml" rel="self" type="application/rss+xml"/><item><title>ArgoCD in Production</title><link>https://blog.cheesedipper.com/p/argocd-in-production/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.cheesedipper.com/p/argocd-in-production/</guid><description>&lt;p&gt;Most kubernetes clusters go a certain way the first time around. You see something cool, you write some manifest files, and you perform a &lt;code&gt;kubectl apply -f deployment.yaml&lt;/code&gt;. This works, is great for debugging, or making sure you got manifests correct. However this means you don&amp;rsquo;t have a source of truth, it&amp;rsquo;s harder to work with others trying to modify the same manifests, and you don&amp;rsquo;t always know if the manifests have been applied.&lt;/p&gt;
&lt;p&gt;One solution to this is GitOps. This allows a git repository to act as a source of truth; constantly checking and updating any running configuration. There are two real choices, &lt;a class="link" href="https://fluxcd.io/" target="_blank" rel="noopener"
 &gt;FluxCD&lt;/a&gt; and &lt;a class="link" href="https://argoproj.github.io/cd/" target="_blank" rel="noopener"
 &gt;ArgoCD&lt;/a&gt;. FluxCD is a very minimal choice - no UI, no RBAC, and less community support. FluxCD is also the choice on &lt;a class="link" href="https://repo1.dso.mil/big-bang/bigbang" target="_blank" rel="noopener"
 &gt;Big Bang&lt;/a&gt; a Department of Defense repository. On the other hand ArgoCD is the opposite — built-in UI and RBAC and great community support.&lt;/p&gt;
&lt;p&gt;Defining ArgoCD applications is pretty easy. Take a look at the manifest below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;apiVersion&lt;/span&gt;: argoproj.io/v1alpha1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: Application
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: adguard &lt;span style="color:#6e738d;font-style:italic"&gt;# the name of the application in the ArgoCD UI&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: argocd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;destination&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;server&lt;/span&gt;: https://kubernetes.default.svc &lt;span style="color:#6e738d;font-style:italic"&gt;# what cluster to deploy to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;project&lt;/span&gt;: default &lt;span style="color:#6e738d;font-style:italic"&gt;# the default project&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;source&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;path&lt;/span&gt;: manifests/adguard/ &lt;span style="color:#6e738d;font-style:italic"&gt;# the relative file path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;repoURL&lt;/span&gt;: https://github.com/ej-east/lisa-cluster &lt;span style="color:#6e738d;font-style:italic"&gt;# target repo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;targetRevision&lt;/span&gt;: HEAD &lt;span style="color:#6e738d;font-style:italic"&gt;# this is the tag to pull, HEAD is latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;syncPolicy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;automated&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;prune&lt;/span&gt;: &lt;span style="color:#f5a97f"&gt;true&lt;/span&gt; &lt;span style="color:#6e738d;font-style:italic"&gt;# removes old resources &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;selfHeal&lt;/span&gt;: &lt;span style="color:#f5a97f"&gt;true&lt;/span&gt; &lt;span style="color:#6e738d;font-style:italic"&gt;# automatically detects and corrects drift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see in ~20 lines you can define an Argo Application. One really cool thing you can do with Argo is create an app-of-apps setup. Defining one app, the root app, to look at a directory of ArgoCD apps and create them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;apiVersion&lt;/span&gt;: argoproj.io/v1alpha1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: Application
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: root-app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: argocd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;finalizers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - resources-finalizer.argocd.argoproj.io
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;destination&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: argocd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;server&lt;/span&gt;: https://kubernetes.default.svc
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;project&lt;/span&gt;: default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;source&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;path&lt;/span&gt;: argocd-applications/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;repoURL&lt;/span&gt;: https://github.com/ej-east/lisa-cluster
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;targetRevision&lt;/span&gt;: HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;syncPolicy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;syncOptions&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - CreateNamespace=true
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;automated&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;prune&lt;/span&gt;: &lt;span style="color:#f5a97f"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;selfHeal&lt;/span&gt;: &lt;span style="color:#f5a97f"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One &lt;code&gt;kubectl apply&lt;/code&gt; to bootstrap, git handles the rest.&lt;/p&gt;
&lt;p&gt;This structure is what makes ArgoCD worth it for me. I can commit everything to git, not worry if what I have locally is running or in GitHub. I no longer have to manually run &lt;code&gt;kubectl apply -f .&lt;/code&gt;, I no longer have to do ClickOps — I can do GitOps.&lt;/p&gt;</description></item><item><title>Secret Management in Kubernetes with Vault and ESO</title><link>https://blog.cheesedipper.com/p/secret-management-in-kubernetes-with-vault-and-eso/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.cheesedipper.com/p/secret-management-in-kubernetes-with-vault-and-eso/</guid><description>&lt;p&gt;When you make a secret in Kubernetes it&amp;rsquo;s not encrypted. It&amp;rsquo;s &lt;em&gt;encoded&lt;/em&gt; in base64. This means anyone can decode your secret and log in as you. It also means you cannot commit your secret to a git repository unless you want others to have the ability to decode it.&lt;/p&gt;
&lt;p&gt;The majority of secrets are meant to be secret. So this method is obviously not going to work for most production environments. Instead &lt;a class="link" href="https://external-secrets.io/latest/" target="_blank" rel="noopener"
 &gt;External Secrets Operator&lt;/a&gt; (ESO) exists. This dynamically pulls secrets from a provider, think of Hashicorp Vault, AWS Secrets Manager, or Google Cloud Secrets Manager, and dynamically creates a Kubernetes secret from it. This allows you to share your manifest without having to worry about accidentally leaking any passwords.&lt;/p&gt;
&lt;p&gt;Another method is &lt;a class="link" href="https://github.com/bitnami-labs/sealed-secrets" target="_blank" rel="noopener"
 &gt;Sealed Secrets&lt;/a&gt; this works with asymmetric encryption - the public key is used to encrypt the secret and the private key, stored on the cluster, is used to decrypt it. This also solves the problem of secret management. Only the people who need to read it can. However there are a few issues with this. First passwords sometimes need to rotate, and most people don&amp;rsquo;t want the additional complexity of a CronJob to update the sealed secret. Secondly you&amp;rsquo;re still committing passwords to git. Which is just bad repository hygiene. Thirdly it&amp;rsquo;s not very production ready. Let&amp;rsquo;s say you get hacked, the pod has a ServiceAccount and the hacker is able to pull the decryption key, and now every sealed secret in the cluster is compromised.&lt;/p&gt;
&lt;p&gt;For these reasons I went with the first option, External Secrets. There are lots of provider options for External Secrets, 1Password, Keeper Security, even ngrok. I went with Hashicorp Vault. This does have some added complexities but it was nice to have it run on cluster as I&amp;rsquo;m trying to have next to no costs for my homelab.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s relatively easy to pull secrets from Hashicorp Vault.&lt;/p&gt;
&lt;h2 id="secrets-like-lisa"&gt;Secrets like LISA
&lt;/h2&gt;&lt;h3 id="first-create-your-secret"&gt;First create your secret!
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vault kv put secret/my-app/config &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;username&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;placeholder&amp;#34;&lt;/span&gt; &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;password&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;placeholder&amp;#34;&lt;/span&gt; &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;api_key&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;placeholder&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="then-create-your-access-policy"&gt;Then create your access policy
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e738d;font-style:italic"&gt;# policy.hcl
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;path&lt;/span&gt; &lt;span style="color:#a6da95"&gt;&amp;#34;secret/data/my-app/config&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; capabilities &lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt; [&lt;span style="color:#a6da95"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vault policy write my-app-read policy.hcl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="then-configure-vaults-kubernetes-auth"&gt;Then configure Vault&amp;rsquo;s Kubernetes auth
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vault auth &lt;span style="color:#91d7e3"&gt;enable&lt;/span&gt; kubernetes &lt;span style="color:#6e738d;font-style:italic"&gt;# you only have to do this once&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vault write auth/kubernetes/config &lt;span style="color:#8aadf4"&gt;\ &lt;/span&gt; &lt;span style="color:#6e738d;font-style:italic"&gt;# you only have to do this once&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;kubernetes_host&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;https://kubernetes.default.svc:443&amp;#34;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vault write auth/kubernetes/role/my-app-role &lt;span style="color:#8aadf4"&gt;\ &lt;/span&gt;&lt;span style="color:#6e738d;font-style:italic"&gt;# do this for each secret &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;bound_service_account_names&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;my-app-sa&amp;#34;&lt;/span&gt; &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;bound_service_account_namespaces&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;my-app&amp;#34;&lt;/span&gt; &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;policies&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;my-app-read&amp;#34;&lt;/span&gt; &lt;span style="color:#8aadf4"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f4dbd6"&gt;ttl&lt;/span&gt;&lt;span style="color:#91d7e3;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a6da95"&gt;&amp;#34;1h&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="then-create-your-secretstore"&gt;Then create your SecretStore
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;apiVersion&lt;/span&gt;: external-secrets.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: SecretStore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: vault-backend
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: my-app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;provider&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;vault&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;server&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;http://hashicorp-vault.hashicorp-vault.svc.cluster.local:8200&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;path&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;secret&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;version&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;v2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;auth&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;kubernetes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;mountPath&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;kubernetes&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;role&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;my-app-role&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;serviceAccountRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;my-app-sa&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="and-pull-the-secret"&gt;And pull the secret!
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;apiVersion&lt;/span&gt;: external-secrets.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: ExternalSecret
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: my-app-secrets
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: my-app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;refreshInterval&lt;/span&gt;: &lt;span style="color:#a6da95"&gt;&amp;#34;1h&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;secretStoreRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: vault-backend
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: SecretStore
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;target&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: my-app-user-password
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;creationPolicy&lt;/span&gt;: Owner
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#c6a0f6"&gt;secretKey&lt;/span&gt;: username
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;remoteRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;key&lt;/span&gt;: my-app/config
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;property&lt;/span&gt;: username
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#c6a0f6"&gt;secretKey&lt;/span&gt;: password
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;remoteRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;key&lt;/span&gt;: my-app/config
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;property&lt;/span&gt;: password
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Congratulations! You just did secret management the production way. ESO automatically refreshes the secret every hour, so if you rotate it in Vault, the pod picks it up. No redeploys required.&lt;/p&gt;</description></item><item><title>Securing traffic to the World Wide Web</title><link>https://blog.cheesedipper.com/p/securing-traffic-to-the-world-wide-web/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.cheesedipper.com/p/securing-traffic-to-the-world-wide-web/</guid><description>&lt;p&gt;There are lots of ways to host a website securely. Originally, I hosted my website on AWS. I used a static S3 site with Route53 for DNS and Cloudfront as a Content Delivery Network(CDN). This worked really well but at the end of the day was static. I couldn&amp;rsquo;t host services like Authentik.&lt;/p&gt;
&lt;p&gt;The solution is simple, get my current cluster on the internet, but doing that securely is hard. My goals: no ongoing cost, end-to-end encryption, and no open ports on my home router.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/cloudflare/cloudflared" target="_blank" rel="noopener"
 &gt;cloudflared&lt;/a&gt; a Cloudflare tunnel client is a great option. It is free, it can run as a pod on my cluster, and I already use Cloudflare to manage my domain. So why not it? Simple - Cloudflare can see the plain text traffic that goes through. This is a giant no-no and breaks one of my requirements. This would be fine for static things but for things like a Wiki or Authentik [anything that has a password] it wouldn&amp;rsquo;t be secure.&lt;/p&gt;
&lt;p&gt;For clarification, Cloudflare isn&amp;rsquo;t doing anything malicious. Their tunnels work by terminating TLS at their edge. Your traffic is encrypted between the request and Cloudflare, and encrypted again between Cloudflare and your server - but in between, Cloudflare can and does see everything in plain text. For a static blog that&amp;rsquo;s fine. For anything with authentication, session tokens, or sensitive data, you&amp;rsquo;re trusting a third party with every request. I didn&amp;rsquo;t want that.&lt;/p&gt;
&lt;p&gt;The solution was to build my own tunnel. I spun up a free-tier Oracle Cloud VM, set up WireGuard between it and my cluster, and configured iptables to forward traffic through. The Oracle VM is a dumb proxy - just forwarding the encrypted traffic. I cover the full setup, config, and iptables rules in &lt;a class="link" href="https://blog.cheesedipper.com/p/how-this-blog-is-hosted/" &gt;How This Blog Is Hosted&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This setup means I own every piece of the path. No vendor sees my traffic in plain text. No Cloudflare outage takes me down. No policy change locks me out. If I want to move domains tomorrow, I update a DNS record and everything still works. The tradeoff is I&amp;rsquo;m responsible for uptime and patching - but that&amp;rsquo;s the point.&lt;/p&gt;</description></item><item><title>Why an IdP is a Necessity</title><link>https://blog.cheesedipper.com/p/why-an-idp-is-a-necessity/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.cheesedipper.com/p/why-an-idp-is-a-necessity/</guid><description>&lt;p&gt;Most homelab setups end the same way. You spin up service after service, set up a username and unique [or worse &lt;em&gt;shared&lt;/em&gt;] password. Before you know it you&amp;rsquo;ve got 15 different logins written down in some notepad, and if you want to add anyone else you&amp;rsquo;ve got no way to centrally manage access.&lt;/p&gt;
&lt;p&gt;On my old cluster this was a major problem of mine. I do come from an IT background and actually have experience with a solution called Okta, an Identity Provider (IdP). Instead of each app having its own username and password they all defer to the IdP. It solves a simple problem, one login for everywhere.&lt;/p&gt;
&lt;p&gt;At first I was looking at &lt;a class="link" href="https://keycloak.org/" target="_blank" rel="noopener"
 &gt;Keycloak&lt;/a&gt;, it&amp;rsquo;s a great choice, it&amp;rsquo;s even used by the &lt;a class="link" href="https://repo1.dso.mil/big-bang/bigbang" target="_blank" rel="noopener"
 &gt;Department of Defense&lt;/a&gt;. For me at least it felt rather bulky and not something I wanted to continue forward with. So I kept looking and found &lt;a class="link" href="https://goauthentik.io/" target="_blank" rel="noopener"
 &gt;Authentik&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Getting Authentik running was straightforward - there is a pre-existing helm chart and it just needs a PostgreSQL database and a Redis instance, both of which I was already running in the cluster. It handles OIDC, SAML, LDAP, and even proxy connections. That last one is worth mentioning because it lets you put auth in front of apps that have zero native SSO support. You just point the proxy provider at the service and suddenly it&amp;rsquo;s behind a login page.&lt;/p&gt;
&lt;p&gt;For each service that I want to have SSO I create a provider in Authentik. This generates a corresponding client ID and client secret. Then you configure the service to use Authentik as its OIDC/SAML provider. The pattern is almost always the same, create a provider in Authentik, configure the application, done. Most applications do take it a bit further with RBAC; you create groups in Authentik that get mapped to certain permissions. For example assigning a user to the &lt;code&gt;sso_grafana_viewer&lt;/code&gt; group would grant them access to view dashboards but not edit them. RBAC is by far one of my favorite features of an IdP.&lt;/p&gt;
&lt;p&gt;The real payoff is when something needs to change. Instead of having to go to each application and review what permissions each user has I can just look in Authentik. I can see everything from what one user can do to what a group of users can do. One action, access everywhere. If I need to revoke it, I can just disable the account in one place and they&amp;rsquo;re immediately locked out. No hunting, no panic.&lt;/p&gt;
&lt;p&gt;An IdP isn&amp;rsquo;t a nice to have feature anymore. It&amp;rsquo;s the difference from fighting access management to controlling it. Once you centralize identity, everything else gets simpler.&lt;/p&gt;</description></item><item><title>How This Blog Is Hosted</title><link>https://blog.cheesedipper.com/p/how-this-blog-is-hosted/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.cheesedipper.com/p/how-this-blog-is-hosted/</guid><description>&lt;p&gt;A homelab is what you make it. You can have the best hardware, fancy GPUs, and tons of memory and still just be running Docker Compose. Does that work? Yes. But it&amp;rsquo;s not how I host things on my homelab. It&amp;rsquo;s not about the resources of the nodes, it&amp;rsquo;s about what you do with them.&lt;/p&gt;
&lt;p&gt;So what makes my homelab different? Hardware-wise not much. I run a 3-node K3s cluster - one master, two workers, each with 16GB of RAM and 512GB of storage. I&amp;rsquo;ve been using K3s since day one. Nothing crazy. K3s is what makes Kubernetes feel like home for me. While it is good to be comfortable with the tools you use, I am planning on switching to &lt;a class="link" href="https://www.talos.dev/" target="_blank" rel="noopener"
 &gt;Talos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I used to have an older version of my homelab. It taught me a lot about the do&amp;rsquo;s and don&amp;rsquo;ts. When I restarted I gave myself a few rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always try to deploy things in the most secure way possible&lt;/li&gt;
&lt;li&gt;No opening ports on the router&lt;/li&gt;
&lt;li&gt;GitOps over everything else&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these shaped how the whole thing is put together. The security one will always be ongoing; there&amp;rsquo;s always new vulnerabilities and each app has something to lock down. But the other two directly dictated the architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="no-open-ports"&gt;No Open Ports
&lt;/h2&gt;&lt;p&gt;&lt;img
 src="https://blog.cheesedipper.com/p/how-this-blog-is-hosted/cluster-to-internet_hu_6647b21cf4972ae0.webp"
 srcset="https://blog.cheesedipper.com/p/how-this-blog-is-hosted/cluster-to-internet_hu_d45b64d87d700909.webp 480w, https://blog.cheesedipper.com/p/how-this-blog-is-hosted/cluster-to-internet_hu_6e93537dd6160113.webp 768w, https://blog.cheesedipper.com/p/how-this-blog-is-hosted/cluster-to-internet_hu_6647b21cf4972ae0.webp 1200w"
 sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1200px"
 width="3281"
 height="1315"
 alt="Traffic flow from the internet through Cloudflare DNS, Oracle VM proxy, and WireGuard tunnel to the cluster"
 loading="lazy"
 decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;This is the rule that feels the strangest. If I don&amp;rsquo;t open any ports on my router, how does traffic actually reach my cluster?&lt;/p&gt;
&lt;p&gt;The answer is a VPN tunnel. I bought a domain, the one you&amp;rsquo;re on right now, &lt;code&gt;cheesedipper.com&lt;/code&gt;, and spun up a small instance in Oracle Cloud using terraform. Oracle has a genuinely good free tier that gives you a VM with a public IP for nothing. It felt like the obvious choice.&lt;/p&gt;
&lt;p&gt;From there I set up &lt;a class="link" href="https://www.wireguard.com/" target="_blank" rel="noopener"
 &gt;WireGuard&lt;/a&gt; between the Oracle instance and my cluster. WireGuard is fast, simple, and the config is small enough to actually understand. The Oracle instance acts as a public-facing entry point. When traffic hits it on port 80 or 443, iptables forwards that traffic through the WireGuard tunnel to my cluster, where Traefik picks it up and routes it to the right service.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the WireGuard config looks like on the Oracle instance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Interface]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Address = &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.1&lt;/span&gt;&lt;span style="color:#ed8796"&gt;/&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ListenPort = &lt;span style="color:#f5a97f"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PrivateKey = &lt;span style="color:#ed8796"&gt;&amp;lt;&lt;/span&gt;privatekey&lt;span style="color:#ed8796"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e738d;font-style:italic"&gt;# Enable forwarding and set up NAT rules when the tunnel comes up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = sysctl -w net.ipv4.ip_forward=&lt;span style="color:#f5a97f"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -t nat -A PREROUTING -i enp0s6 -p tcp --dport &lt;span style="color:#f5a97f"&gt;80&lt;/span&gt; -j DNAT --to-destination &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.2&lt;/span&gt;&lt;span style="color:#ed8796"&gt;:&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -t nat -A PREROUTING -i enp0s6 -p tcp --dport &lt;span style="color:#f5a97f"&gt;443&lt;/span&gt; -j DNAT --to-destination &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.2&lt;/span&gt;&lt;span style="color:#ed8796"&gt;:&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -A FORWARD -i enp0s6 -o wg0 -p tcp --dport &lt;span style="color:#f5a97f"&gt;80&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -A FORWARD -i enp0s6 -o wg0 -p tcp --dport &lt;span style="color:#f5a97f"&gt;443&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostUp = iptables -A FORWARD -i wg0 -o enp0s6 -m state --state RELATED,ESTABLISHED -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e738d;font-style:italic"&gt;# Clean up when the tunnel goes down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -t nat -D PREROUTING -i enp0s6 -p tcp --dport &lt;span style="color:#f5a97f"&gt;80&lt;/span&gt; -j DNAT --to-destination &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.2&lt;/span&gt;&lt;span style="color:#ed8796"&gt;:&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -t nat -D PREROUTING -i enp0s6 -p tcp --dport &lt;span style="color:#f5a97f"&gt;443&lt;/span&gt; -j DNAT --to-destination &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.2&lt;/span&gt;&lt;span style="color:#ed8796"&gt;:&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -D FORWARD -i enp0s6 -o wg0 -p tcp --dport &lt;span style="color:#f5a97f"&gt;80&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -D FORWARD -i enp0s6 -o wg0 -p tcp --dport &lt;span style="color:#f5a97f"&gt;443&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PostDown = iptables -D FORWARD -i wg0 -o enp0s6 -m state --state RELATED,ESTABLISHED -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e738d;font-style:italic"&gt;# My cluster node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[Peer]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PublicKey = &lt;span style="color:#ed8796"&gt;&amp;lt;&lt;/span&gt;publickey&lt;span style="color:#ed8796"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;AllowedIPs = &lt;span style="color:#f5a97f"&gt;10.200&lt;/span&gt;.&lt;span style="color:#f5a97f"&gt;0.2&lt;/span&gt;&lt;span style="color:#ed8796"&gt;/&lt;/span&gt;&lt;span style="color:#f5a97f"&gt;32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a request comes in on the Oracle VM&amp;rsquo;s network interface (&lt;code&gt;enp0s6&lt;/code&gt;) on port 80 or 443, the PREROUTING rule rewrites the destination to &lt;code&gt;10.200.0.2&lt;/code&gt; — which is my cluster node on the other side of the WireGuard tunnel. The MASQUERADE rule makes sure return traffic knows how to get back. The FORWARD rules allow the traffic to pass between interfaces, and the RELATED,ESTABLISHED rule lets response traffic flow back out.&lt;/p&gt;
&lt;p&gt;The PostDown rules are just the cleanup - they remove everything when WireGuard shuts down so you don&amp;rsquo;t end up with stale iptables rules.&lt;/p&gt;
&lt;p&gt;My home IP is never exposed. DNS points to the Oracle VM, and the Oracle VM forwards everything to my cluster through an encrypted tunnel. From the outside, it looks like everything is hosted in Oracle Cloud.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="tls-certificates-with-cert-manager"&gt;TLS Certificates with cert-manager
&lt;/h2&gt;&lt;p&gt;With traffic flowing through the tunnel, the next problem was TLS. I need HTTPS, don&amp;rsquo;t want to rely on Cloudflare, and don&amp;rsquo;t want to deal with certificates manually.&lt;/p&gt;
&lt;p&gt;I use &lt;a class="link" href="https://cert-manager.io/" target="_blank" rel="noopener"
 &gt;cert-manager&lt;/a&gt; with Let&amp;rsquo;s Encrypt. The important choice here is the challenge type. There are two options: HTTP-01 and DNS-01. HTTP-01 requires Let&amp;rsquo;s Encrypt to reach your server on port 80 to verify you own the domain. DNS-01 verifies ownership by checking a DNS TXT record.&lt;/p&gt;
&lt;p&gt;I went with DNS-01 for a simple reason - it felt the easiest. My DNS is managed through &lt;a class="link" href="https://www.cloudflare.com/" target="_blank" rel="noopener"
 &gt;Cloudflare&lt;/a&gt;, and cert-manager has a built-in Cloudflare solver. I give cert-manager a Cloudflare API token, it creates the TXT record, Let&amp;rsquo;s Encrypt verifies it, and the certificate gets issued. No extra ports, no special ingress rules for the challenge. The whole thing happens through the Cloudflare API, which means cert-manager doesn&amp;rsquo;t need any inbound traffic at all to get a certificate.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#cad3f5;background-color:#24273a;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;apiVersion&lt;/span&gt;: cert-manager.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;kind&lt;/span&gt;: ClusterIssuer
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: letsencrypt-prod
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;namespace&lt;/span&gt;: kube-system
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#c6a0f6"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;acme&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;server&lt;/span&gt;: https://acme-v02.api.letsencrypt.org/directory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;email&lt;/span&gt;: ej@cheesedipper.com 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;privateKeySecretRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: letsencrypt-prod-account-key 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;solvers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#c6a0f6"&gt;dns01&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;cloudflare&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;apiTokenSecretRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;name&lt;/span&gt;: cloudflare-api-token
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#c6a0f6"&gt;key&lt;/span&gt;: api-token
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the cert is issued, cert-manager stores it as a Kubernetes secret that Traefik picks up automatically. Certificates renew on their own, so TLS is one of those things I set up once and can safely forget.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="gitops-with-argocd"&gt;GitOps with ArgoCD
&lt;/h2&gt;&lt;p&gt;The third rule - GitOps over everything - is the one that changed how I think about managing a cluster.&lt;/p&gt;
&lt;p&gt;I went with &lt;a class="link" href="https://argo-cd.readthedocs.io/" target="_blank" rel="noopener"
 &gt;ArgoCD&lt;/a&gt; over Flux because of the UI, the RBAC model, and the workflow in general. My philosophy: everything that runs on my cluster is defined in a Git repo. ArgoCD watches that repo and makes sure the cluster matches what&amp;rsquo;s in Git. If I want to deploy something, I push a commit. If I want to roll something back, I revert a commit. I only run &lt;code&gt;kubectl apply&lt;/code&gt; against the cluster when testing, to make sure the working code is always in git.&lt;/p&gt;
&lt;p&gt;I use the &lt;a class="link" href="https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/" target="_blank" rel="noopener"
 &gt;App of Apps&lt;/a&gt; pattern. There&amp;rsquo;s a root ArgoCD Application that points to a directory of other Application manifests. Each of those points to either a Helm chart or a set of plain Kubernetes manifests for a specific app. When I want to add something new to the cluster, I add an Application manifest to the repo and ArgoCD picks it up.&lt;/p&gt;
&lt;p&gt;Some apps use Helm charts - things like cert-manager, gatekeeper, and ArgoCD itself where the upstream chart handles the complexity. Other apps that I&amp;rsquo;ve written or that are simpler just use plain manifests. The mix works fine. ArgoCD doesn&amp;rsquo;t care how the manifests are generated, it just syncs them.&lt;/p&gt;
&lt;p&gt;The thing I like most about this setup is that my cluster is reproducible. If something goes wrong, the Git repo is the source of truth. I could wipe the cluster, reinstall K3s, point ArgoCD at the repo, and everything comes back.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="how-it-all-fits-together"&gt;How It All Fits Together
&lt;/h2&gt;&lt;p&gt;When you loaded this page, your browser resolved &lt;code&gt;cheesedipper.com&lt;/code&gt; to the Oracle VM&amp;rsquo;s public IP. The request hit the VM on port 443, iptables forwarded it through the WireGuard tunnel to my cluster, and Traefik routed it to the right service. The response came back through the same tunnel. Your browser has no idea it&amp;rsquo;s talking to a server in my house.&lt;/p&gt;
&lt;p&gt;Everything running on the cluster is defined in Git. Certs renew through the Cloudflare API. My home IP never shows up anywhere public.&lt;/p&gt;
&lt;p&gt;Is it overkill for a blog? Probably. But the blog isn&amp;rsquo;t really the point; showcasing secure, scalable infrastructure is.&lt;/p&gt;</description></item></channel></rss>