<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 3deep5me</title>
    <description>The latest articles on DEV Community by 3deep5me (@3deep5me).</description>
    <link>https://dev.to/3deep5me</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1832602%2Fb9fa1622-2ba3-4492-ac74-770f7ee44b6b.jpeg</url>
      <title>DEV Community: 3deep5me</title>
      <link>https://dev.to/3deep5me</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/3deep5me"/>
    <language>en</language>
    <item>
      <title>Giving Kubernetes ServiceAccounts an identity in Forgejo with oauth2-proxy</title>
      <dc:creator>3deep5me</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:01:11 +0000</pubDate>
      <link>https://dev.to/3deep5me/giving-kubernetes-serviceaccounts-an-identity-in-forgejo-with-oauth2-proxy-1po6</link>
      <guid>https://dev.to/3deep5me/giving-kubernetes-serviceaccounts-an-identity-in-forgejo-with-oauth2-proxy-1po6</guid>
      <description>&lt;p&gt;In my little agent platform every coding agent is a Kubernetes Job: it gets a repository, a task and a deadline, works, pushes a branch and opens a pull request. Which means every one of those Jobs has to authenticate against Forgejo, our Git host - and my first version of that was exactly the thing you'd expect: a Personal Access Token per agent, sitting in a SealedSecret next to the agent definition.&lt;/p&gt;

&lt;p&gt;It worked. I still didn't like it, and the reasons were never "it's a secret in Git", because it wasn't a secret in Git - it was sealed. The reasons were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The credential outlives the work.&lt;/strong&gt; An agent run lasts two hours. The token lasts until somebody remembers to revoke it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a shared identity, not a per-run one.&lt;/strong&gt; Anybody who can create a Job can mount the SealedSecret. That includes a fellow agent, and it means "who pushed this?" has exactly one answer for the whole platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a value somewhere.&lt;/strong&gt; In a pod spec, in &lt;code&gt;env&lt;/code&gt;, in a crash dump, in a diff of a debug session. A credential that never exists as a value cannot leak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of managing a token per agent, I made the agents log in as themselves. Kubernetes already knows who a pod is - a ServiceAccount - and it will happily mint a signed token for it. That token is a JWT with a &lt;code&gt;sub&lt;/code&gt; claim. Forgejo accepts an HTTP header as an authenticated user if you ask it to. And oauth2-proxy is a small piece of software that verifies JWTs.&lt;/p&gt;

&lt;p&gt;All three pieces exist. The interesting part is the glue - and the diagram below is that glue: an agent pod whose only credential is a projected token, a gate with two doors, and a Forgejo that nothing but the gate may reach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pr7thbmgq0oshv4boib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2pr7thbmgq0oshv4boib.png" alt="The gate: the pod's projected token (aud=forgejo) is verified by oauth2-proxy, the verified Kubernetes subject is mapped to a Forgejo account, and only the gate pod may open a connection to the instance" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The pods: an identity, not a secret
&lt;/h1&gt;

&lt;p&gt;The pod gets one volume. That's the entire credential configuration of an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forgejo-identity&lt;/span&gt;
    &lt;span class="na"&gt;projected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;serviceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;              &lt;span class="c1"&gt;# /var/run/secrets/forgejo/token&lt;/span&gt;
            &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forgejo&lt;/span&gt;        &lt;span class="c1"&gt;# what makes it acceptable to the gate&lt;/span&gt;
            &lt;span class="na"&gt;expirationSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="c1"&gt;# longer than any run may be, and no longer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;audience&lt;/code&gt; is the entire point. A normal ServiceAccount token is minted for the API server. A token with &lt;code&gt;audience: forgejo&lt;/code&gt; is useless against the API server and only usable by whoever accepts that audience - which is the thing in front of Forgejo. It is a scoping mechanism, and it costs one line.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;projected&lt;/code&gt; is the other half: the kubelet writes this file and &lt;strong&gt;rewrites it in place&lt;/strong&gt; while the token approaches expiry. So nothing in the container may freeze its value. The entrypoint refuses to start without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PI_FORGEJO_SA_TOKEN_FILE&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;log &lt;span class="s2"&gt;"fatal: no projected ServiceAccount token - refusing to run unauthenticated"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd rather have a run that does not happen than a run that silently talks to Forgejo unauthenticated.&lt;/p&gt;

&lt;p&gt;The pod doesn't write the token anywhere either - git is authenticated by a wrapper that is put on &lt;code&gt;PATH&lt;/code&gt; and reads the file on &lt;strong&gt;every&lt;/strong&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;REAL_GIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; git&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /agent-bin/git &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;WRAPPER&lt;/span&gt;&lt;span class="sh"&gt;
#!/bin/sh
exec "&lt;/span&gt;&lt;span class="nv"&gt;$REAL_GIT&lt;/span&gt;&lt;span class="sh"&gt;" -c "http.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PI_FORGEJO_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/.extraHeader=Authorization: Bearer &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;(cat "&lt;/span&gt;&lt;span class="nv"&gt;$PI_FORGEJO_SA_TOKEN_FILE&lt;/span&gt;&lt;span class="sh"&gt;")" "&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;@"
&lt;/span&gt;&lt;span class="no"&gt;WRAPPER
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the header is scoped to our Forgejo URL, the token is never offered to any other host; because it is read per call, a rotation in the middle of a two-hour run is invisible. Nothing lands in &lt;code&gt;.git/config&lt;/code&gt; and no credential helper is involved - which also means a &lt;code&gt;git&lt;/code&gt; command that fails cannot print a credential in its error message.&lt;/p&gt;

&lt;h1&gt;
  
  
  The cluster as an identity provider
&lt;/h1&gt;

&lt;p&gt;This is the part that surprised me the most: the Kubernetes API server is a working OIDC issuer, and you can check the claims yourself without installing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create token hermes-pke &lt;span class="nt"&gt;--audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;forgejo &lt;span class="nt"&gt;--duration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10m &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get &lt;code&gt;iss&lt;/code&gt; (the API server), &lt;code&gt;aud: ["forgejo"]&lt;/code&gt;, an expiry, and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:serviceaccount:hermes-pke:hermes-pke"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That subject &lt;em&gt;is&lt;/em&gt; the identity: namespace and ServiceAccount name, unforgeable, signed with the cluster's own keys, expiring on its own.&lt;/p&gt;

&lt;p&gt;The catch is that the API server is a &lt;strong&gt;verification-only&lt;/strong&gt; issuer. Its discovery document advertises no authorization and no token endpoint, because there is nothing to authorize - you don't log into Kubernetes, you present a token Kubernetes gave you. Every OIDC client library that insists on doing a full discovery will therefore fail against it, and you have to tell it to skip discovery and fetch the signing keys directly.&lt;/p&gt;

&lt;h1&gt;
  
  
  oauth2-proxy as the verifier
&lt;/h1&gt;

&lt;p&gt;oauth2-proxy normally does "log in with GitHub/Google" for a web app. It also understands bearer tokens, and that mode is what we use: it takes the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header, checks signature, issuer, audience and expiry against the API server's own keys, and forwards the verified username to its upstream.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--provider=oidc&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--oidc-issuer-url=https://kubernetes.default.svc.cluster.local&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--skip-oidc-discovery=true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--oidc-jwks-url=https://kubernetes.default.svc.cluster.local/openid/v1/jwks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--provider-ca-file=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--client-id=forgejo&lt;/span&gt;            &lt;span class="c1"&gt;# = the audience the token must carry&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--user-id-claim=sub&lt;/span&gt;            &lt;span class="c1"&gt;# a ServiceAccount has no e-mail address&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--email-domain=*&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--skip-jwt-bearer-tokens=true&lt;/span&gt;  &lt;span class="c1"&gt;# accept the bearer token, no browser session&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--bearer-token-login-fallback=false&lt;/span&gt;  &lt;span class="c1"&gt;# invalid token =&amp;gt; 403, not a redirect&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--pass-user-headers=true&lt;/span&gt;       &lt;span class="c1"&gt;# hand the subject on as X-Forwarded-User&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--upstream=http://127.0.0.1:8081/&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--http-address=0.0.0.0:4180&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four of those lines are the ones I'd keep if I could only keep four: &lt;code&gt;--client-id&lt;/code&gt; (a token for another audience is rejected), &lt;code&gt;--user-id-claim=sub&lt;/code&gt; (there is no e-mail to claim), &lt;code&gt;--skip-jwt-bearer-tokens&lt;/code&gt; (this is what makes a Kubernetes token a login at all) and &lt;code&gt;--bearer-token-login-fallback=false&lt;/code&gt; - with this issuer there is no interactive flow to redirect to, so an invalid token must be a hard 403.&lt;/p&gt;

&lt;p&gt;Why oauth2-proxy and not Forgejo itself verifying the token? Because Forgejo, like most applications, does not grow custom token verification for my homelab. It has one standard mechanism for "something in front of me authenticated this request", and I'd rather use the standard mechanism with a dedicated verifier than patch the application. Which brings us to the interesting part.&lt;/p&gt;

&lt;h1&gt;
  
  
  Forgejo as the relying party
&lt;/h1&gt;

&lt;p&gt;Forgejo supports reverse-proxy authentication: if a request arrives with a certain header, that header value is the username. Turned on in &lt;code&gt;app.ini&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[service]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;ENABLE_REVERSE_PROXY_AUTHENTICATION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true      # the web UI&lt;/span&gt;
&lt;span class="py"&gt;ENABLE_REVERSE_PROXY_AUTHENTICATION_API&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true  # and the API - agents use this&lt;/span&gt;
&lt;span class="py"&gt;ENABLE_REVERSE_PROXY_AUTO_REGISTRATION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false  # never invent an account&lt;/span&gt;
&lt;span class="py"&gt;ENABLE_REVERSE_PROXY_EMAIL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;ENABLE_REVERSE_PROXY_FULL_NAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[security]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;REVERSE_PROXY_AUTHENTICATION_USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-User&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-registration off is deliberate: an account that the gate can present must already exist, with its rights declared in the repository that manages them. An unknown subject stays an unauthenticated request.&lt;/p&gt;

&lt;p&gt;Now the part I measured rather than assumed, and the reason this post is worth writing: &lt;strong&gt;in Forgejo 15.0.10 there is no trusted-proxy check for this method.&lt;/strong&gt; The reverse-proxy login code takes the header on faith. &lt;code&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/code&gt; exists, but in this version it only influences the source address used for logging - it does not gate the login.&lt;/p&gt;

&lt;p&gt;Which means the header is a login credential, and &lt;strong&gt;reachability is part of the authentication&lt;/strong&gt;: whoever can open a TCP connection to Forgejo's HTTP port can be whoever they like. Two consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Forgejo's Service is a &lt;strong&gt;ClusterIP&lt;/strong&gt;, and a Cilium network policy allows exactly one pod to speak to it - the gate. Re-opening that port for anything else is a hole, so the "extra allowed CIDRs" list in my values file is empty and documented as such.&lt;/li&gt;
&lt;li&gt;My own gate must not be reachable in a way that lets someone skip the verifier.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hence the gate is a pod with &lt;strong&gt;two doors&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:8080   the classic door   - browsers, the CI runner, Forgejo tokens
                            BLANKS every identity header a client sends
:8081   the identity door  - only this pod's own oauth2-proxy may reach it
                            (enforced by the network policy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both doors proxy to the same Forgejo. The classic door deletes the headers, always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-User&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-WEBAUTH-USER&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Auth-Request-User&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Email&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not "overwrites if trusted" - blanks. A client sending those headers is not lying more convincingly, it is simply ignored. Without this line, the whole design collapses back into "become anyone by sending a header".&lt;/p&gt;

&lt;p&gt;The identity door is the one oauth2-proxy forwards to, and it strips the token on the way through, because the token has done its job by then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-User&lt;/span&gt; &lt;span class="nv"&gt;$identity_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Authorization&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  One header, many subjects
&lt;/h1&gt;

&lt;p&gt;Now the cosmetic problem: the subject arriving from Kubernetes is &lt;code&gt;system:serviceaccount:agent-reviewer:reviewer&lt;/code&gt;. Forgejo usernames cannot contain colons, and I don't want a name pattern to create accounts - an account must exist, and it must be the &lt;em&gt;right&lt;/em&gt; account.&lt;/p&gt;

&lt;p&gt;So the identity door maps the subject to a login, with a default that fails closed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_x_forwarded_user&lt;/span&gt; &lt;span class="nv"&gt;$identity_user&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;"system:serviceaccount:agent-tofu:tofu"&lt;/span&gt;        &lt;span class="s"&gt;"agent-tofu"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;"system:serviceaccount:hermes-pke:hermes-pke"&lt;/span&gt;  &lt;span class="s"&gt;"agent-hermes-pke"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;"system:serviceaccount:agent-reviewer:reviewer"&lt;/span&gt; &lt;span class="s"&gt;"agent-reviewer"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything not listed - including an empty header, including something that merely &lt;em&gt;looks&lt;/em&gt; like a ServiceAccount - becomes an empty string, i.e. an unauthenticated request. No account is ever invented from a name pattern.&lt;/p&gt;

&lt;p&gt;Adding an agent is therefore two reviewed lines in two repositories: one line in this table (which Kubernetes subject may act as which Forgejo account) and one row in the access repository (what that account may do in which repository). Revoking an agent is deleting the line. That asymmetry - the identity table is small, explicit and reviewable - is what I actually wanted from all of this, much more than "no secret".&lt;/p&gt;

&lt;h1&gt;
  
  
  What bit me
&lt;/h1&gt;

&lt;p&gt;Because a post that only shows the working end state is useless, here are the failures, with the errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cookie_secret must be 16, 24 or 32 bytes, but is 44&lt;/code&gt;.&lt;/strong&gt; Even in bearer-token mode oauth2-proxy validates the cookie config. My SealedSecret held a base64 &lt;em&gt;string&lt;/em&gt; (44 chars) instead of the 32 bytes it decodes to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;502 from the gate.&lt;/strong&gt; oauth2-proxy's upstream pointed at the Forgejo &lt;strong&gt;pod&lt;/strong&gt;'s port (:3000) instead of the &lt;strong&gt;Service&lt;/strong&gt; (:80). Use the Service - it is what a chart should depend on, and it survives rescheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;host not found in upstream&lt;/code&gt;&lt;/strong&gt;, but only on the node where the new pod landed. The network policy's DNS egress rule didn't match: &lt;code&gt;toEndpoints.matchLabels&lt;/code&gt; matches &lt;strong&gt;inside the policy's own namespace&lt;/strong&gt;, so &lt;code&gt;k8s-app: kube-dns&lt;/code&gt; needs Cilium's own &lt;code&gt;io.kubernetes.pod.namespace: kube-system&lt;/code&gt; label next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx died silently&lt;/strong&gt; in a read-only root filesystem. Every temp path it would create under &lt;code&gt;/var/cache/nginx&lt;/code&gt; has to be redirected onto an &lt;code&gt;emptyDir&lt;/code&gt; mounted at &lt;code&gt;/tmp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The web UI's live updates died&lt;/strong&gt; at the gate until I forwarded &lt;code&gt;Upgrade&lt;/code&gt;/&lt;code&gt;Connection&lt;/code&gt; for websockets - and &lt;code&gt;client_max_body_size 0&lt;/code&gt;, because a &lt;code&gt;git push&lt;/code&gt; is not a small request and must not be buffered onto a small disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My first cut had the identity door open to the whole cluster.&lt;/strong&gt; Anyone could have sent the header straight to &lt;code&gt;:8081&lt;/code&gt; and skipped oauth2-proxy entirely. The gate's own network policy now allows that port from its own pod only - which is the same rule as before, just applied to the other door.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Why not the obvious alternatives
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A token per agent (PAT).&lt;/strong&gt; Works, and it is how I started. Long-lived, shared by whoever can create a Job, present as a value, and revocation is a human task. The Git identity of an agent should not be something a human has to remember to take away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy keys.&lt;/strong&gt; Nice for a push-only robot, but they are per repository and they are not a &lt;em&gt;user&lt;/em&gt;: no attribution, no pull-request authorship, no reviews. With n agents and m repositories you also get n×m keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username + password for the agent account.&lt;/strong&gt; The same as a PAT, with a worse rotation story and a real login session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the wire.&lt;/strong&gt; That's the trap above, not an alternative.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  What it costs, what it buys
&lt;/h1&gt;

&lt;p&gt;Costs: one additional hop in front of Git (the gate), an identity table to maintain, and the discipline that only the gate may reach the instance. Both gates are ordinary Kubernetes objects, so all of it is in Git and reconciled.&lt;/p&gt;

&lt;p&gt;Buys: no credential exists as a value anywhere - not in Git, not in a pod, not in a backup; each agent run is itself; a runaway agent is revoked by deleting one line; every commit, branch and pull request is attributable to the agent that made it; and the identity of a Job is the same identity the cluster already uses for its RBAC, which means there is exactly one story about "who is this" in the platform instead of two.&lt;/p&gt;

&lt;p&gt;Next on my list: an agent that starts the runs of &lt;em&gt;another&lt;/em&gt; agent (a reviewer that reviews every pull request), and what that costs in admission control. That's a post of its own. 🙂&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tested against Forgejo 15.0.10 (chart 17.3.1), a Cilium-based cluster and oauth2-proxy in bearer-token mode. The reverse-proxy behaviour without a trusted-proxy check is a measured fact about this version - if you rely on it, measure it on yours.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you're doing something similar - or you know a Forgejo version that gates the reverse-proxy header properly - I'd love to hear it in the comments.&lt;/p&gt;

&lt;p&gt;This post was completely written by LLM also the architecture and wiring behind this. It's only validated by a human if it works. (It works!) &lt;br&gt;
But tbh I don't know if it's secure - so before you use it in production you should verify it by yourself - because rbac is nothing to play with. &lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>From Zero to Scale: Proxmox Kubernetes Engine</title>
      <dc:creator>3deep5me</dc:creator>
      <pubDate>Fri, 02 May 2025 13:53:31 +0000</pubDate>
      <link>https://dev.to/3deep5me/from-zero-to-scale-kubernetes-on-proxmox-the-scaling-autopilot-method-1l64</link>
      <guid>https://dev.to/3deep5me/from-zero-to-scale-kubernetes-on-proxmox-the-scaling-autopilot-method-1l64</guid>
      <description>&lt;p&gt;&lt;strong&gt;If you are looking for a enterprise solution for Kubernetes on Proxmox check out my available subscriptions and support - which evolved out of the project ❤️.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.proxmox-kubernetes-engine.eu/" rel="noopener noreferrer"&gt;https://www.proxmox-kubernetes-engine.eu/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Today, we'll take a beginner-friendly look at housing Kubernetes in Proxmox. But instead of the traditional SSH/Ansible approach, we'll explore a method akin to what you'd find with AWS, Azure, or GCP. This means we're talking about scaling from tens to hundreds of Kubernetes clusters in minutes, with automated, reproducible cluster creation and upgrades.&lt;/p&gt;

&lt;p&gt;Does that sound like it requires heavy modifications to your Proxmox hosts or datacenter? I can reassure you: I dislike straying far from default settings, so &lt;strong&gt;you won't need to modify your Proxmox installation in any way&lt;/strong&gt;. It's simply a virtual machine, allowing you to add and remove it like a plugin. (More on the architecture later.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do You Need This?
&lt;/h2&gt;

&lt;p&gt;I don't need this personally — it's just a bit of fun to replicate big cloud provider functionality in my tiny homelab!&lt;/p&gt;

&lt;p&gt;However, if your company lacks a scalable Kubernetes platform, you might find it tough to keep up in today's service-oriented world. With major cloud providers dominating, efficiently managing a private cloud is more crucial than ever, and Kubernetes is one of the most popular cloud tools. So, how can you compete? The answer lies in two of my favorite open-source projects: Cluster-API and Proxmox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proxmox
&lt;/h3&gt;

&lt;p&gt;Proxmox, developed in Vienna, is widely known in the open-source and home-lab communities. It gained a huge boost in small to medium-scale private clouds after VMware's new pricing model alienated its customers. It's a simple yet powerful open-source hypervisor based on KVM, and it's been the core of my home lab for nearly a decade (I've been using it since PVE 4).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster-API
&lt;/h3&gt;

&lt;p&gt;While Cluster API might be less known in the home-lab community, it's highly valued in enterprises and by Kubernetes administrators and enthusiasts. So, what exactly is Cluster API?&lt;/p&gt;

&lt;p&gt;Cluster API is a project under the Cloud Native Computing Foundation (CNCF), strongly supported by the Kubernetes community and various vendors, including VMware, Apple, and NVIDIA. This project offers a unified way to create and manage Kubernetes clusters across different "providers," such as Proxmox or VMware. For instance, VMware heavily leverages Cluster API in its commercial product, Tanzu.&lt;/p&gt;

&lt;p&gt;Cluster API currently boasts over 30 infrastructure providers, with Proxmox being just one of them. In short, Cluster API provides a unified API and method for creating production-ready Kubernetes clusters across numerous providers. It has become, at least for me, the de facto standard for multi-cloud and on-premises Kubernetes deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

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

&lt;p&gt;Let me introduce you to the architecture we will use for our setup. Of course, we need at least one Proxmox host to rely on; I'll be using the newest version. On top of Proxmox, we will have our main component: the Management VM. The Management VM will house Cluster API.&lt;/p&gt;

&lt;p&gt;The Management VM is then responsible for the Kubernetes clusters we want to create. For example, the Management VM automatically coordinates the creation and updates of Kubernetes clusters.&lt;/p&gt;

&lt;p&gt;The Management VM achieves this by instructing Proxmox to automatically create and remove specially configured VMs on your Proxmox infrastructure. The process in detail would look like this:&lt;/p&gt;

&lt;p&gt;We will give our Management VM the order to create, for instance, a 12-node Kubernetes cluster. The Management VM, or more precisely, Cluster API inside the Management VM, will communicate with Proxmox. Cluster API will then instruct Proxmox to provision 12 VMs from a special Kubernetes VM template. After the successful creation of the VMs, Cluster API configures the cluster. Additionally, Cluster API will monitor the VMs and can replace them automatically upon failure.&lt;/p&gt;

&lt;p&gt;You can see that this setup is far more capable than statically creating clusters with tools like Terraform and Ansible. This is why setups like this are often referred to as a Kubernetes Platform - because you can order clusters just like you order a pizza from Lieferando.&lt;/p&gt;

&lt;p&gt;In my opinion, it's also a less complicated way to set up multi-node Kubernetes clusters on Proxmox. We simply need to install Cluster API on our Management VM and have a Kubernetes VM template ready. No complex Ansible Playbooks or Terraform-states are required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our Path to a Scalable Kubernetes Platform on Proxmox
&lt;/h3&gt;

&lt;p&gt;In this blog post, we'll follow these steps to achieve our goal of creating a scalable Kubernetes platform on Proxmox:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the Management VM as our pivotal point.&lt;/li&gt;
&lt;li&gt;Create a Kubernetes VM Template for our Kubernetes Clusters.&lt;/li&gt;
&lt;li&gt;Initialize Cluster API &amp;amp; Configure the "Caprox-Kubernetes-Engine."&lt;/li&gt;
&lt;li&gt;Create our first Workload Cluster.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In my example, the Management VM and Kubernetes clusters will all reside on a single Proxmox host within one network that utilizes DHCP. DHCP is a requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Guide still works - but it is a bit outdated. I recommend to check out the GitHub Project which evolved out of this blogpost. Here is the updated Tutorial: &lt;a href="https://github.com/Caprox-eu/Proxmox-Kubernetes-Engine/blob/main/docs/quick-start.md" rel="noopener noreferrer"&gt;Proxmox-Kubernetes-Engine/Quick-Start&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Management VM with k3sup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: All the steps shown here are for home-lab purposes only and &lt;strong&gt;should not be used for a production or even development environment&lt;/strong&gt;. This guide is merely a starting point. If you're looking for a more production-ready setup, feel free to reach out to me.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a New VM for Our Management VM
&lt;/h3&gt;

&lt;p&gt;We need a simple Linux VM for our &lt;strong&gt;Management VM&lt;/strong&gt; with &lt;strong&gt;30 GiB of disk space&lt;/strong&gt; and at least &lt;strong&gt;4 GiB of RAM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For Proxmox beginners, I recommend &lt;a href="https://support.us.ovhcloud.com/hc/en-us/articles/360010916620-How-to-Create-a-VM-in-Proxmox-VE" rel="noopener noreferrer"&gt;this great guide&lt;/a&gt;. You'll also need an ISO image, which you can download &lt;a href="https://ubuntu.com/download/server/thank-you?version=24.04.2&amp;amp;architecture=amd64&amp;amp;lts=true" rel="noopener noreferrer"&gt;here&lt;/a&gt; from the official Ubuntu website.&lt;/p&gt;

&lt;p&gt;Of course, you can also use other methods such as Terraform, cloud-init or cloning an existing Linux VM or template.&lt;/p&gt;

&lt;p&gt;Once you've logged into your new VM, we can proceed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Kubernetes (K3s) on Our New VM
&lt;/h3&gt;

&lt;p&gt;Cluster API is Kubernetes-exclusive, meaning it itself relies on Kubernetes to operate. Because of this, we need Kubernetes on our Management VM. In this guide, we'll use &lt;strong&gt;K3s&lt;/strong&gt; for that. K3s is a Kubernetes distribution from Rancher that is now fully community-driven and under the CNCF. It's a popular Kubernetes distro for small to mid-sized clusters.&lt;/p&gt;

&lt;p&gt;To install K3s, we'll use &lt;strong&gt;&lt;code&gt;k3sup&lt;/code&gt;&lt;/strong&gt; - a really simple CLI tool that allows you to create a K3s Kubernetes cluster within seconds on any Linux VM. The following commands will download &lt;code&gt;k3sup&lt;/code&gt; and then create a simple cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install k3s-Kubernetes with k3sup&lt;/span&gt;
curl &lt;span class="nt"&gt;-sLS&lt;/span&gt; https://get.k3sup.dev | sh
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;k3sup /usr/local/bin/k3sup
k3sup &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="nt"&gt;--k3s-version&lt;/span&gt; v1.33.1+k3s1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test if the cluster is working with &lt;code&gt;sudo k3s kubectl get nodes&lt;/code&gt;. It should show something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get nodes
NAME        STATUS   ROLES                  AGE     VERSION
localhost   Ready    control-plane,master   6m30s   v1.33.1+k3s1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! You've successfully set up a simple single-node Kubernetes cluster. This cluster will serve as our Cluster API Management VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Your Kubernetes VM Template
&lt;/h2&gt;

&lt;p&gt;Every Kubernetes cluster typically starts with a machine template, most often a &lt;strong&gt;VM template&lt;/strong&gt;. This template is where all Kubernetes components are configured and downloaded. For Cluster API, we need VM templates that already include essential Kubernetes packages like the API server and the container runtime.&lt;/p&gt;

&lt;p&gt;We'll achieve this with the help of the excellent &lt;a href="https://github.com/kubernetes-sigs/image-builder/tree/main" rel="noopener noreferrer"&gt;Kubernetes Image Builder&lt;/a&gt; project. While it sounds fancy, it's essentially &lt;strong&gt;Packer and Ansible&lt;/strong&gt; cleverly glued together to automate the image creation process. But as promised, you don't need to touch Ansible or Packer at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create an API Token and a Secret with the Values
&lt;/h3&gt;

&lt;p&gt;To get started, we need to create API access to our Proxmox Datacenter. You might wonder why. The image builder constructs the image directly on your Proxmox node, so it requires access to start a VM and later convert it into a template.&lt;/p&gt;

&lt;p&gt;To begin, open a shell on your Proxmox node. You can do this by clicking on your Proxmox node in the interface and selecting "Shell"; a command-line interface will then open directly in your browser. Once the shell is open, execute the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pveum user add caprox@pve
pveum aclmod / &lt;span class="nt"&gt;-user&lt;/span&gt; caprox@pve &lt;span class="nt"&gt;-role&lt;/span&gt; PVEAdmin
pveum user token add caprox@pve capi &lt;span class="nt"&gt;-privsep&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that you should get all relevant information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node01:~# pveum user token add caprox@pve capi &lt;span class="nt"&gt;-privsep&lt;/span&gt; 0
┌──────────────┬──────────────────────────────────────┐
│ key          │ value                                │
╞══════════════╪══════════════════════════════════════╡
│ full-tokenid │ caprox@pve!capi                      │
├──────────────┼──────────────────────────────────────┤
│ info         │ &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"privsep"&lt;/span&gt;:&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;                      │
├──────────────┼──────────────────────────────────────┤
│ value        │ 6e59df15-a2c9-4dc5-b293-367772950c68 │
└──────────────┴──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will also use our Management VM to coordinate the VM Template Builds. For that we will use the Kubernetes Job Resource and a Kubernetes Secret to pass the API-Key and some configuration.&lt;/p&gt;

&lt;p&gt;First let us create the Secret with values we got from above.&lt;br&gt;
You only need to change the first four Proxmox-Vars.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# secret.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-image-build-config&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-build-infrastructure-system&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-proxmox-adress:8006/api2/json"&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full-tokenid"&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value"&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_NODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;proxmox&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_ISO_POOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local"&lt;/span&gt; &lt;span class="c1"&gt;#this should be fine for the most users&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_BRIDGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vmbr0"&lt;/span&gt; &lt;span class="c1"&gt;#this should be fine for the most users&lt;/span&gt;
  &lt;span class="na"&gt;PROXMOX_STORAGE_POOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local"&lt;/span&gt; &lt;span class="c1"&gt;#this should be fine for the most users&lt;/span&gt;
  &lt;span class="na"&gt;PACKER_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory=4096&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'kubernetes_rpm_version=1.33.1'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'kubernetes_semver=v1.33.1'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'kubernetes_series=v1.33'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'kubernetes_deb_version=1.33.1-1.1'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure needed values and save the File.&lt;/p&gt;

&lt;p&gt;Now we need also a Job which creates the Image. A Job is a Kubernetes Pod which is only executed once with a finite life - until the Job successfully complete. It uses Image-Builder Docker-Image with the required configuration to build Kubernetes Proxmox VM-Templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# job.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CronJob&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-template-builder&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-build-infrastructure-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;suspend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; 
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
          &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;image-builder&lt;/span&gt;
            &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.k8s.io/scl-image-builder/cluster-node-image-builder-amd64:v0.1.45&lt;/span&gt;
            &lt;span class="na"&gt;envFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-image-build-config&lt;/span&gt;
            &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build-proxmox-ubuntu-2404&lt;/span&gt;
            &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# help for slow nodes&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ANSIBLE_TIMEOUT&lt;/span&gt;
                &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;60"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just copy and save the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start the Build-Job and create a Kubernetes Image
&lt;/h3&gt;

&lt;p&gt;Now you should have two file one Secret and one (Cron)-Job, the Cron-Job has the benefit of easier executing if we need new image or even do scheduled automatic builds. Now its build time!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# create a namespace for the build&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl create namespace proxmox-build-infrastructure-system
&lt;span class="c"&gt;# apply secret &amp;amp; job&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; secret.yaml
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; job.yaml
&lt;span class="c"&gt;# start the build&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl create job build-image &lt;span class="nt"&gt;--from&lt;/span&gt; cj/proxmox-template-builder &lt;span class="nt"&gt;-n&lt;/span&gt; proxmox-build-infrastructure-system 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To Monitor the current state we can look at the Logs of the Builder-Pod. For that we need the name and the &lt;code&gt;kubectl logs&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; proxmox-build-infrastructure-system
NAME                READY   STATUS    RESTARTS   AGE
build-image-tdv78   1/1     Running   0          97s
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; proxmox-build-infrastructure-system build-image-tdv78
proxmox-iso.ubuntu-2204: output will be &lt;span class="k"&gt;in &lt;/span&gt;this color.
&lt;span class="o"&gt;==&amp;gt;&lt;/span&gt; proxmox-iso.ubuntu-2204: Retrieving ISO
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a new VM pop-up in your Proxmox UI. This is often the trickiest part of the guide. Sometimes it can take a long time, or the job might even require multiple attempts. Kubernetes will automatically restart the job if the build fails, so you can just relax and let it do its thing. In my experience, it occasionally took over 30 minutes to create the VM template.&lt;/p&gt;

&lt;p&gt;Please ensure that you have DHCP configured in your network. If you encounter any issues, feel free to leave a comment below; I'm happy to help! Sometimes a Proxmox reboot also helps.&lt;/p&gt;

&lt;p&gt;If you're lucky, you should see a new template in the UI after a few minutes.&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febymhlsjwkqa2o49bhek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Febymhlsjwkqa2o49bhek.png" alt=" " width="480" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And thats it - you completed the "most complicated" part!&lt;/p&gt;

&lt;h2&gt;
  
  
  Initialize Cluster-API
&lt;/h2&gt;

&lt;p&gt;In the next step, we'll configure Cluster-API to our needs. I prefer to avoid keeping files, especially complex ones, directly on my PC. For this reason, we'll use &lt;strong&gt;ArgoCD&lt;/strong&gt; with a &lt;strong&gt;GitOps workflow&lt;/strong&gt; to install Cluster-API. New to GitOps and ArgoCD? Don't worry, it's straightforward!&lt;/p&gt;

&lt;h3&gt;
  
  
  Install ArgoCD and get cluster-api
&lt;/h3&gt;

&lt;p&gt;ArgoCD will retrieve the YAML configurations for the various Cluster-API components. You can install ArgoCD using these two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl create namespace argocd
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; argocd &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check the status of argocd with this command &lt;code&gt;sudo k3s kubectl get pods -n argocd&lt;/code&gt; if all pods are running you are ready for the next step.&lt;/p&gt;

&lt;p&gt;We will install these Cluster-API components as ArgoCD Applications. An ArgoCD Application is similar to something you might know from your Google Play Store or Apple App Store – it's a packaged application or set of applications. Just as you download and install an app on your phone, ArgoCD manages the deployment and lifecycle of these Cluster-API components onto your Kubernetes clusters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes-sigs/cluster-api-operator" rel="noopener noreferrer"&gt;Cluster-API Operator&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;The operator is responsible for installing Cluster-API and configuring it for Proxmox.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cert-manager.io/" rel="noopener noreferrer"&gt;Cert-Manager&lt;/a&gt;.

&lt;ul&gt;
&lt;li&gt;Cert-Manager is utilized by Cluster-API to automatically generate and manage certificates required for the Kubernetes clusters.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Caprox-eu/Proxmox-Kubernetes-Engine/tree/main/manifests/clusterclass-cilium-with-shared-ippool/base" rel="noopener noreferrer"&gt;Caprox-Engine&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;This application includes my opinionated ClusterClass, which has been designed with the needs of home labs and Small and Medium-sized Businesses (SMBs) in mind. A ClusterClass acts as a customizable template, significantly simplifying the process of creating and managing Kubernetes clusters.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app-cert-manager.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-api-operator-cert-manager&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt; 
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;capi-operator-system&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://charts.jetstack.io&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.17.2&lt;/span&gt;
    &lt;span class="na"&gt;chart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager&lt;/span&gt;
    &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;installCRDs: true&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ServerSideApply=true&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app-cluster-api.yaml&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-api-operator-main&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;capi-operator-system&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes-sigs.github.io/cluster-api-operator&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.19.0&lt;/span&gt;
    &lt;span class="na"&gt;chart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-api-operator&lt;/span&gt;
    &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;manager:&lt;/span&gt;
          &lt;span class="s"&gt;featureGates:&lt;/span&gt;
            &lt;span class="s"&gt;proxmox:&lt;/span&gt;
              &lt;span class="s"&gt;ClusterTopology: true&lt;/span&gt;
            &lt;span class="s"&gt;core:&lt;/span&gt;
              &lt;span class="s"&gt;ClusterTopology: true&lt;/span&gt;
            &lt;span class="s"&gt;kubeadm:&lt;/span&gt;
              &lt;span class="s"&gt;ClusterTopology: true&lt;/span&gt;
        &lt;span class="s"&gt;core:&lt;/span&gt;
          &lt;span class="s"&gt;cluster-api:&lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v1.10.2&lt;/span&gt;
        &lt;span class="s"&gt;bootstrap:&lt;/span&gt;
          &lt;span class="s"&gt;kubeadm: &lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v1.10.2&lt;/span&gt;
        &lt;span class="s"&gt;controlPlane: &lt;/span&gt;
          &lt;span class="s"&gt;kubeadm: &lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v1.10.2&lt;/span&gt;
        &lt;span class="s"&gt;infrastructure: &lt;/span&gt;
          &lt;span class="s"&gt;proxmox:&lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v0.7.1&lt;/span&gt;
        &lt;span class="s"&gt;ipam:&lt;/span&gt;
          &lt;span class="s"&gt;in-cluster:&lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v1.0.1&lt;/span&gt;
        &lt;span class="s"&gt;addon:&lt;/span&gt;
          &lt;span class="s"&gt;helm: &lt;/span&gt;
            &lt;span class="s"&gt;enabled: true&lt;/span&gt;
            &lt;span class="s"&gt;version: v0.3.1&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ServerSideApply=true&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app-caprox-engine.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster-api-operator-caprox-engine&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt; 
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;capi-operator-system&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/Caprox-eu/Proxmox-Kubernetes-Engine.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;99c87250802d886cfce28fe20a313637eae8a80a&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manifests/clusterclass-cilium-with-shared-ippool/base&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ServerSideApply=true&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;ignoreDifferences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster.x-k8s.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterClass&lt;/span&gt;
      &lt;span class="na"&gt;jsonPointers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/spec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save these files and apply them one by one. This sequential application is necessary because the applications depend on each other, and we need to ensure each one is ready before proceeding to the next.&lt;/p&gt;

&lt;p&gt;Lets start with the Cert-Manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; app-cert-manager.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the App is Heathly and synced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get apps &lt;span class="nt"&gt;-A&lt;/span&gt;
NAME                                 SYNC STATUS   HEALTH STATUS
cluster-api-operator-cert-manager    Synced        Healthy
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then go on with Cluster-API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; app-cluster-api.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the App is Heathly and synced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get apps &lt;span class="nt"&gt;-A&lt;/span&gt;
NAME                                 SYNC STATUS   HEALTH STATUS
cluster-api-operator-cert-manager    Synced        Healthy
cluster-api-operator-main            Synced        Healthy
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then with the caprox-engine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; app-caprox-engine.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the App is Heathly and synced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get apps &lt;span class="nt"&gt;-A&lt;/span&gt;
NAME                                 SYNC STATUS   HEALTH STATUS
cluster-api-operator-caprox-engine   Synced        Healthy
cluster-api-operator-cert-manager    Synced        Healthy
cluster-api-operator-main            Synced        Healthy
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice! Now we need to configure cluster-api to our specific environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect Cluster-API to Proxmox
&lt;/h3&gt;

&lt;p&gt;Cluster-API needs access to your Proxmox cluster to create VMs for Kubernetes clusters. For a homelab setup, we can simply reuse the Proxmox credentials you created in the "Create an API Token and Create a Secret with the Values" step.&lt;/p&gt;

&lt;p&gt;Create a secret with the exact same structure, configure it for your environment, then save and apply it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# secret-capmox.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6e59df15-a2c9-4dc5-b293-367772950c68"&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caprox@pve!capi"&lt;/span&gt;
  &lt;span class="c1"&gt;# note: only the host - not the api-path&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://192.168.2.142:8006/"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;capmox-manager-credentials&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-infrastructure-system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; secret-capmox.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we are overwriting the default secret we need to trigger a restart of the proxmox-provider to read the new secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl rollout restart deploy capmox-controller-manager &lt;span class="nt"&gt;-n&lt;/span&gt; proxmox-infrastructure-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure IP Range for the Cluster-API Caprox Kubernetes Engine
&lt;/h3&gt;

&lt;p&gt;The Virtual Machines (VMs) provisioned by Proxmox/Cluster-API will require IP addresses. Currently, DHCP support is &lt;a href="https://github.com/ionos-cloud/cluster-api-provider-proxmox/issues/29" rel="noopener noreferrer"&gt;not available&lt;/a&gt;. However, we can specify an IP pool for our Kubernetes VMs. In my setup, I've disabled DHCP for a range of IPs within my FritzBox. (Note: It's recommended to disable DHCP for more IPs than you initially anticipate needing – more on this later.)&lt;/p&gt;

&lt;p&gt;Copy this file, configure it for your network, then save and apply it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ip-pool.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ipam.cluster.x-k8s.io/v1alpha2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InClusterIPPool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clusterclass-ipv4&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caprox-kubernetes-engine&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Change the IP range to match your needs&lt;/span&gt;
  &lt;span class="c1"&gt;# These IPs will be used for the Kubernetes nodes&lt;/span&gt;
  &lt;span class="c1"&gt;# Also configure your network prefix and gateway accordingly&lt;/span&gt;
  &lt;span class="na"&gt;addresses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.2.150-192.168.2.199&lt;/span&gt;
  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.2.1&lt;/span&gt;
  &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ip-pool.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We've successfully installed ArgoCD on our Management VM, used ArgoCD to install Cluster-API, and configured Cluster-API for our environment. Now we're finally ready to create our first multi-node Kubernetes cluster!&lt;/p&gt;

&lt;h2&gt;
  
  
  Create our first Workload Cluster
&lt;/h2&gt;

&lt;p&gt;As always everthing is a file - same is true for a Kubernetes Cluster in Cluster-API. So yeah we will create a Kubernetes Cluster from Cluster YAML. The file will be later proccesed by Cluster-API within our Management VM.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cluster Resource
&lt;/h3&gt;

&lt;p&gt;A cluster configuration which is compatible with our setup could look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cluster.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster.x-k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;caprox.eu/cni&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cilium-v1.17.4&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manuels-k8s-cluster&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caprox-kubernetes-engine&lt;/span&gt; 
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;topology&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-clusterclass-cilium-v0.1.0&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.33.1&lt;/span&gt;
    &lt;span class="na"&gt;controlPlane&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;machineDeployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-worker&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;proxmox-worker-pool&lt;/span&gt;
        &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloneSpec&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;vmTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sourceNode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node01&lt;/span&gt;
          &lt;span class="na"&gt;templateID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;114&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;controlPlaneEndpoint&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.2.201&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields you &lt;strong&gt;must&lt;/strong&gt; change are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;vmTemplate&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sourceNode&lt;/code&gt;&lt;/strong&gt;: Set this to the name of the Proxmox node where your VM template is located.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;templateID&lt;/code&gt;&lt;/strong&gt;: This is the ID of your Kubernetes template. You can find it in the Proxmox UI, listed as the number next to the template name.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;controlPlaneEndpoint&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This IP address will be used to access your Kubernetes cluster.&lt;/li&gt;
&lt;li&gt;Ensure it's outside your defined IP pool range.&lt;/li&gt;
&lt;li&gt;This is a floating IP, shared among different nodes. This ensures your cluster remains reachable even during node failures or maintenance, as long as at least one node is available.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fields you &lt;strong&gt;can&lt;/strong&gt; change are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;controlplane.replicas&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This specifies the number of VMs for your control plane (master) nodes. These nodes host Kubernetes control plane components like the Kubernetes API and etcd (the Kubernetes database).&lt;/li&gt;
&lt;li&gt;Set this to &lt;code&gt;1&lt;/code&gt; for a non-HA (High Availability) setup, or &lt;code&gt;3&lt;/code&gt; or &lt;code&gt;5&lt;/code&gt; for an HA setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;workers.replicas&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This determines the number of VMs that will run your workloads.&lt;/li&gt;
&lt;li&gt;Set this to at least &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;metadata.name&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is the name of your Kubernetes cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more configuration options, including memory and CPU settings for your VMs, refer to the available &lt;strong&gt;variables&lt;/strong&gt; &lt;a href="https://github.com/3deep5me/kubernetes-gitops/blob/cluster-api-action/k3s-mgmt-proxmox/manifest/capmox-setup/cilium-clusterclass-with-shared-ippool/base/variables/clonespec.yaml" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After you've set the required fields, remember to save the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scaling and New Clusters
&lt;/h3&gt;

&lt;p&gt;If you want to &lt;strong&gt;scale out&lt;/strong&gt; your current cluster, simply adjust the &lt;strong&gt;node replica counts&lt;/strong&gt; in your configuration file. Then, reapply the changes using &lt;code&gt;kubectl apply -f cluster.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To create an &lt;strong&gt;additional cluster&lt;/strong&gt;, create a new file with a different &lt;strong&gt;&lt;code&gt;metadata.name&lt;/code&gt;&lt;/strong&gt; and set your desired node replica counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Workload Cluster and Retrieve the Kubeconfig
&lt;/h3&gt;

&lt;p&gt;To create the cluster, we need to "send" our cluster configuration to our Management VM. This action will kick off the cluster creation process, which is often referred to as &lt;strong&gt;reconciliation&lt;/strong&gt;. After the first control plane VM is created and started, we can retrieve the &lt;strong&gt;kubeconfig&lt;/strong&gt; for our new cluster. The kubeconfig file contains all the necessary login information to access the created cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the Cluster&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cluster.yaml

&lt;span class="c"&gt;# Retrieve and save the kubeconfig&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get secrets &lt;span class="nt"&gt;-n&lt;/span&gt; caprox-kubernetes-engine manuels-k8s-cluster-kubeconfig &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.value}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;--decode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubeconfig.yaml

&lt;span class="c"&gt;# Connect to the new cluster&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./kubeconfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can verify the connection by listing the nodes in your new cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This command should now show your multiple nodes, depending on your configuration.&lt;/span&gt;
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
NAME                                            STATUS   ROLES           AGE     VERSION
manuels-k8s-cluster-control-plane-4z458-ghx2z   Ready    control-plane   11m     v1.33.1
manuels-k8s-cluster-worker-6vhjx-c2nv2-v4dbw    Ready    node            5m23s   v1.33.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to interact with the Management VM/Cluster API again, you'll need to unset the environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;unset &lt;/span&gt;KUBECONFIG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! You've successfully completed the whole process. You can now &lt;strong&gt;dynamically create Kubernetes clusters in your Proxmox datacenter in an API-driven way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you'd like a guide on how to update your existing cluster, please let me know in the comments!&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;p&gt;There are many reasons why cluster creation might fail. I've tried to address all common errors within the template and reduce the setup steps to make the process as lean as possible. All versions and dependencies are fixed. I've tested this guide on multiple Proxmox datacenters and also asked friends to test it, and it has been successful every time.&lt;/p&gt;

&lt;p&gt;However, if you encounter problems, it's likely that you either missed a step or didn't follow the guide exactly as described. Please feel free to leave a comment if you struggle or don't understand any steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Now that your Kubernetes engine is configured, what can you do with it? Here are some common next steps I like to take on newly created Kubernetes clusters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/3deep5me/using-1password-with-external-secrets-operator-in-a-gitops-way-4lo4"&gt;Set up 1Password with a external secret manager&lt;/a&gt; for secure secret management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install an ingress controller&lt;/strong&gt; to manage external access to cluster services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure load balancing in Cilium&lt;/strong&gt; for multi-node load balancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install applications&lt;/strong&gt; like databases or Pi-hole using Helm.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Trivia
&lt;/h3&gt;

&lt;p&gt;For this blog post, the following PRs/comments/issues were contributed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Caprox-eu/Proxmox-Kubernetes-Engine" rel="noopener noreferrer"&gt;https://github.com/Caprox-eu/Proxmox-Kubernetes-Engine&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes-sigs/image-builder/pull/1778" rel="noopener noreferrer"&gt;https://github.com/kubernetes-sigs/image-builder/pull/1778&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ionos-cloud/cluster-api-provider-proxmox/pull/499" rel="noopener noreferrer"&gt;https://github.com/ionos-cloud/cluster-api-provider-proxmox/pull/499&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ionos-cloud/cluster-api-provider-proxmox/issues/492" rel="noopener noreferrer"&gt;https://github.com/ionos-cloud/cluster-api-provider-proxmox/issues/492&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/image-builder/issues/1762" rel="noopener noreferrer"&gt;https://github.com/kubernetes-sigs/image-builder/issues/1762&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>proxmox</category>
      <category>homelab</category>
      <category>devops</category>
    </item>
    <item>
      <title>Using 1Password with External Secrets Operator in a GitOps way</title>
      <dc:creator>3deep5me</dc:creator>
      <pubDate>Tue, 30 Jul 2024 15:35:57 +0000</pubDate>
      <link>https://dev.to/3deep5me/using-1password-with-external-secrets-operator-in-a-gitops-way-4lo4</link>
      <guid>https://dev.to/3deep5me/using-1password-with-external-secrets-operator-in-a-gitops-way-4lo4</guid>
      <description>&lt;p&gt;I recently created a &lt;a href="https://gist.github.com/3deep5me/86ce9a0a2691d21d69684b01432bc1f6" rel="noopener noreferrer"&gt;free Kubernetes cluster&lt;/a&gt; on Oracles Always Free Tier. But how to handle secrets in a secure way, especially in a GitOps scenario?&lt;/p&gt;

&lt;p&gt;This post will describe the full journey how to integrate the External Secrets Operator with 1Password to automatically inject secrets from 1Password to your cluster. For managing the different components, I will use argocd, but you can use also just helm install or FluxCD.&lt;/p&gt;

&lt;p&gt;My Goal was to have a enterprise-known solution which integrates with different secret-managment tools, in my case it's 1Password.&lt;/p&gt;

&lt;h1&gt;
  
  
  External Secrets Operator
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;External Secrets Operator is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM Cloud Secrets Manager, CyberArk Conjur and many more. The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret.&lt;/em&gt;&lt;br&gt;
-external-secrets.io&lt;/p&gt;

&lt;p&gt;I like the fact that i can theoretically change my secret-management-tool without touching my secrets. Its also possible to have the same Secret-File in all my deployments over different stages. Because the secret-value gets filled regarding to the current stage. These are all things which were often a pain for me with sealed-secrets.&lt;/p&gt;
&lt;h1&gt;
  
  
  1Password
&lt;/h1&gt;

&lt;p&gt;Really comfy Password-manager.&lt;br&gt;
I have it so why not also using it for K8s?&lt;/p&gt;
&lt;h1&gt;
  
  
  Architecture
&lt;/h1&gt;

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

&lt;p&gt;Let us have a look on the architecture especially the mechanics between external secret operator with Kubernetes and 1Password with the external secret operator.&lt;/p&gt;
&lt;h2&gt;
  
  
  External Secret Operator
&lt;/h2&gt;

&lt;p&gt;The External Secret Operator uses in general two &lt;a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/" rel="noopener noreferrer"&gt;Custom Resources&lt;/a&gt;. The first is the &lt;strong&gt;SecretStore&lt;/strong&gt; which holds information, how to connect to the "Secret Backend". The "Secret Backend" can be something like AWS Secrets Manager, Scaleway or like in our case 1Password. It holds for example the URL and the authentication for it. You can have different SecretStores if you have more than one provider. The other resource is the &lt;strong&gt;ExternalSecret&lt;/strong&gt; which is a blueprint for the secret which should be created. So in practice if you create an ExternalSecret a normal K8s-Secret follows. The ExternalSecret holds information about which entry in 1Password to fetch and in which way to populate the values in a Secret. (More later)&lt;/p&gt;
&lt;h2&gt;
  
  
  1Password Connect
&lt;/h2&gt;

&lt;p&gt;1Password-Connect is a kind-of Proxy which caches Secrets from the 1Password Cloud-Service, so you are able to retrieve secrets even 1Password is down, it also reduces the amount of requests to 1Password.&lt;br&gt;
Its needed for the External Secrets Operator to work.&lt;br&gt;
In my approach, it is right next to the External Secret Operator in the same namespace.&lt;/p&gt;

&lt;p&gt;All together the workflow looks similar to that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;External Secret is created&lt;/li&gt;
&lt;li&gt;External Secret Operator uses the information's in SecretStore to fetch the needed password/login from 1Password Connect&lt;/li&gt;
&lt;li&gt;1Password connect itself fetch the needed password/login from 1Password directly&lt;/li&gt;
&lt;li&gt;External Secret Operator gets the password/login &amp;amp; creates the normal Kubernetes Secret with the requested values&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't worry if you don't understand the whole process, we'll do a full hands-on walk-through at the end.&lt;/p&gt;
&lt;h1&gt;
  
  
  Install needed Helm-Charts
&lt;/h1&gt;

&lt;p&gt;For the Installation of the helm-charts i will use argocd-apps, because i find this approach really portable. If i have a new cluster i just install argocd and then a &lt;code&gt;kubectl apply&lt;/code&gt; . and i have all my needed apps installed. Beyond that i can use kustomize or helm and i have a auto-update feature of the apps - if i want to. Just set the &lt;code&gt;targetRevision&lt;/code&gt; field to something like this &lt;code&gt;1.x.x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But you can also use helm or fluxcd.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install 1Password-Connect
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Install the helm-chart
&lt;/h3&gt;

&lt;p&gt;I modified the default values of the official &lt;a href="https://github.com/1Password/connect-helm-charts/tree/main/charts/connect" rel="noopener noreferrer"&gt;Chart&lt;/a&gt; to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# values.yaml
connect:
  serviceType: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default value is NodePort which is IMO dangerous if you publish your Secret-management-api to the local network or even the Internet. ClusterIP is suffient because the External Secrets Operator is in the cluster - not outside.&lt;/p&gt;

&lt;p&gt;To install the Chart with the predefined values you can just apply this argocd-app. (&lt;code&gt;kubectl apply -f app-1password-connect.yaml&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app-1password-connect.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: 1password-connect
  namespace: argocd 
spec:
  destination:
    namespace: external-secrets
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://1password.github.io/connect-helm-charts
    targetRevision: 1.x.x
    chart: connect
    helm:
      values: |
        connect:
          serviceType: ClusterIP
  syncPolicy:
    automated: {}
    syncOptions:
    - CreateNamespace=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that you should have a failed Pod in the external-secrets namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pods -n external-secrets
onepassword-connect-5fcbd4c68b-fgx68                         0/2     CreateContainerConfigError   0             4d20h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is &lt;code&gt;Error: secret "op-credentials" not found&lt;/code&gt; (&lt;code&gt;kubectl get event -n external-secrets&lt;/code&gt;). This Secret contains the token which is used for the connect-server to connect to 1Password.&lt;/p&gt;

&lt;p&gt;To create the Secret install the &lt;a href="https://1password.com/downloads/command-line/" rel="noopener noreferrer"&gt;1Password CLI&lt;/a&gt; and execute following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create a new Vault for the Secrets
$ op vault create "K8s"
# Create a connect server in 1Password
op connect server create "Kubernetes" --vaults "K8s"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The location of the credentials file can be seen in the output.&lt;/p&gt;

&lt;p&gt;With the 1password-credentials.json in place we can create the needed secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# The connect service expects the 1password-credentials.json in base64
$ kubectl create secret generic op-credentials -n external-secrets --from-literal=1password-credentials.json="$(cat /path/to/1password-credentials.json | base64)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a while the pod should be running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pod -n external-secrets
onepassword-connect-5fcbd4c68b-chmfg                         2/2     Running   0
  21m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations you completed the first step - installed the 1Password-Connect Server in Kubernetes!&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the External Secret Operator
&lt;/h2&gt;

&lt;p&gt;For installation you can use again an argocd-app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets-operator
  namespace: argocd 
spec:
  destination:
    namespace: external-secrets
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://charts.external-secrets.io
    targetRevision: 0.x.x
    chart: external-secrets
  syncPolicy:
    automated: {}
    syncOptions:
    - CreateNamespace=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that your namespace should look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pods -n external-secrets
NAME                                                         READY   STATUS    RESTARTS      AGE
external-secrets-operator-5db95c68b8-j6pzj                   1/1     Running   1 (18d ago)   18d
external-secrets-operator-cert-controller-65f74dc7bf-784hg   1/1     Running   1 (18d ago)   18d
external-secrets-operator-webhook-5475bddd67-p9vq9           1/1     Running   0             18d
onepassword-connect-5fcbd4c68b-chmfg                         2/2     Running   0             31m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can establish the connection to the connect server (höhö).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create the token and save it in the OP_CONNECT_TOKEN environment variable 
export OP_CONNECT_TOKEN=$(op connect token create "external-secret-operator" --server "Kubernetes" --vault "K8s")
# Create secret with the token which is used by the External-Secret-Operator ClusterSecretStore
kubectl create secret -n external-secrets generic onepassword-connect-token --from-literal=token=$OP_CONNECT_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the ClusterSecretStore with &lt;code&gt;kubectl apply -f&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: k8s
spec:
  provider:
    onepassword:
      connectHost: http://onepassword-connect:8080
      vaults:
        K8s: 1  # look in this vault first
      auth:
        secretRef:
          connectTokenSecretRef:
            name: onepassword-connect-token
            key: token
            namespace: external-secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see if it the connection was successful you can check the status of the ClusterSecretStore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get -n external-secrets clustersecretstores.external-secrets.io k8s
NAME   AGE    STATUS   CAPABILITIES   READY
k8s    103m   Valid    ReadOnly       True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations you also installed the external secret operator and established a connection with 1password connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  End-to-End-Test the Solution
&lt;/h2&gt;

&lt;p&gt;Theoretically you should be able to retrieve secrets from 1Password - but let us test it with a pragmatical example.&lt;/p&gt;

&lt;p&gt;First let us create a secret in 1Password with the 1Password CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# This creates a new entry named "Scaleway Credentails" with two properties/fields. 
op item create --vault="K8s" --title="Scaleway Credentials" --category="login" accessKeyId="token-xyz" secretKey="xyz"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the next step we can create our ExternalSecret which then creates our normal Kubernetes Secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  # name of the ExternalSecret &amp;amp; Secret which gets created
  name: scaleway-credentials
spec:
  secretStoreRef:
    kind: ClusterSecretStore
    name: k8s
  target:
    creationPolicy: Owner
  data:
  - secretKey: accessKeyId
    remoteRef:
      # 1password-entry-name
      key: "Scaleway Credentials"
      # 1password-field
      property: accessKeyId
  - secretKey: secretKey
    remoteRef:
      # 1password-entry-name
      key: "Scaleway Credentials"
      # 1password-field
      property: secretKey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried my best to explain the references with comments.&lt;br&gt;
To summarize the name of the ExternalSecret do not count. But its important that the secretKey is and the property field has the same value. The secretKey and property field represent the "Field" in 1Password e.g. password or user. The key field represents the entry in 1Password e.g. dev.to.&lt;/p&gt;

&lt;p&gt;You can check the status of the external secret and also if the normal K8s secret was created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get externalsecret -n default
NAME                   STORE   REFRESH INTERVAL   STATUS         READY
scaleway-credentials   k8s     1h                 SecretSynced   True
$kubectl get secret -n default
NAME                   TYPE     DATA   AGE
scaleway-credentials   Opaque   2      7m13s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see the secret is created. You can also have a deeper look into the values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get secret -n default -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    accessKeyId: dG9rZW4teHl6
    secretKey: eHl6
  immutable: false
  kind: Secret
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"external-secrets.io/v1beta1","kind":"ExternalSecret","metadata":{"annotations":{},"name":"scaleway-credentials","namespace":"default"},"spec":{"data":[{"remoteRef":{"key":"Scaleway Credentials","property":"accessKeyId"},"secretKey":"accessKeyId"},{"remoteRef":{"key":"Scaleway Credentials","property":"secretKey"},"secretKey":"secretKey"}],"secretStoreRef":{"kind":"ClusterSecretStore","name":"k8s"},"target":{"creationPolicy":"Owner"}}}
      reconcile.external-secrets.io/data-hash: f3d79dd2ad3055723d0bebe3481f2372
    creationTimestamp: "2024-07-29T15:36:04Z"
    labels:
      reconcile.external-secrets.io/created-by: c66895ad2c04350c73c512c0175cee24
    name: scaleway-credentials
    namespace: default
    ownerReferences:
    - apiVersion: external-secrets.io/v1
      blockOwnerDeletion: true
      controller: true
      kind: ExternalSecret
      name: scaleway-credentials
      uid: 1df5f0f5-ec44-4cb7-859a-3d3ef60b1d45
    resourceVersion: "689603"
    uid: 9543092a-7bdc-4695-9c83-bc35c36188c0
  type: Opaque
kind: List
metadata:
  resourceVersion: ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under &lt;code&gt;data&lt;/code&gt; you can see the both keys and the corresponding values encoded in base64.&lt;/p&gt;

&lt;p&gt;Congratulations you successfully created a secret in 1Password and pulled it into a normal Kubernetes Secret which can now be used as usual in deployments and other resources. 🥳🥳🥳&lt;/p&gt;

&lt;p&gt;Thanks for reading my first post!&lt;br&gt;
I hope you had fun and that it helped you. I'd love to hear your thoughts or feedback, so feel free to leave a comment below.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>opensource</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
